mirror of
https://github.com/revanced/revanced-manager-compose.git
synced 2025-04-30 06:14:25 +02:00
feat: finish implementing the sources system (#70)
This commit is contained in:
parent
1ba97b3e4c
commit
d08f6f9ed8
@ -2,11 +2,11 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "f7e0fef1b937143a8b128e3dbab7c041",
|
"identityHash": "7142188e25ce489eb233aed8fb76e4cc",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "sources",
|
"tableName": "patch_bundles",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `location` TEXT NOT NULL, `version` TEXT NOT NULL, `integrations_version` TEXT NOT NULL, PRIMARY KEY(`uid`))",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, `version` TEXT, `integrations_version` TEXT, PRIMARY KEY(`uid`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "uid",
|
"fieldPath": "uid",
|
||||||
@ -21,22 +21,28 @@
|
|||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "location",
|
"fieldPath": "source",
|
||||||
"columnName": "location",
|
"columnName": "source",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "autoUpdate",
|
||||||
|
"columnName": "auto_update",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "versionInfo.patches",
|
"fieldPath": "versionInfo.patches",
|
||||||
"columnName": "version",
|
"columnName": "version",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "versionInfo.integrations",
|
"fieldPath": "versionInfo.integrations",
|
||||||
"columnName": "integrations_version",
|
"columnName": "integrations_version",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
@ -47,20 +53,20 @@
|
|||||||
},
|
},
|
||||||
"indices": [
|
"indices": [
|
||||||
{
|
{
|
||||||
"name": "index_sources_name",
|
"name": "index_patch_bundles_name",
|
||||||
"unique": true,
|
"unique": true,
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"name"
|
"name"
|
||||||
],
|
],
|
||||||
"orders": [],
|
"orders": [],
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_sources_name` ON `${TABLE_NAME}` (`name`)"
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_bundles_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"foreignKeys": []
|
"foreignKeys": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "patch_selections",
|
"tableName": "patch_selections",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `source` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`source`) REFERENCES `sources`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "uid",
|
"fieldPath": "uid",
|
||||||
@ -69,8 +75,8 @@
|
|||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "source",
|
"fieldPath": "patchBundle",
|
||||||
"columnName": "source",
|
"columnName": "patch_bundle",
|
||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
@ -89,23 +95,23 @@
|
|||||||
},
|
},
|
||||||
"indices": [
|
"indices": [
|
||||||
{
|
{
|
||||||
"name": "index_patch_selections_source_package_name",
|
"name": "index_patch_selections_patch_bundle_package_name",
|
||||||
"unique": true,
|
"unique": true,
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"source",
|
"patch_bundle",
|
||||||
"package_name"
|
"package_name"
|
||||||
],
|
],
|
||||||
"orders": [],
|
"orders": [],
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_source_package_name` ON `${TABLE_NAME}` (`source`, `package_name`)"
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"foreignKeys": [
|
"foreignKeys": [
|
||||||
{
|
{
|
||||||
"table": "sources",
|
"table": "patch_bundles",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"columns": [
|
"columns": [
|
||||||
"source"
|
"patch_bundle"
|
||||||
],
|
],
|
||||||
"referencedColumns": [
|
"referencedColumns": [
|
||||||
"uid"
|
"uid"
|
||||||
@ -189,7 +195,7 @@
|
|||||||
"views": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f7e0fef1b937143a8b128e3dbab7c041')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7142188e25ce489eb233aed8fb76e4cc')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,6 +5,7 @@
|
|||||||
<permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
<permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="ReservedSystemPermission" />
|
tools:ignore="ReservedSystemPermission" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
@ -7,7 +7,7 @@ import androidx.compose.animation.ExperimentalAnimationApi
|
|||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||||
import app.revanced.manager.ui.destination.Destination
|
import app.revanced.manager.ui.destination.Destination
|
||||||
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
||||||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||||
@ -28,22 +28,19 @@ import dev.olshevski.navigation.reimagined.popUpTo
|
|||||||
import dev.olshevski.navigation.reimagined.rememberNavController
|
import dev.olshevski.navigation.reimagined.rememberNavController
|
||||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||||
import org.koin.android.ext.android.get
|
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val prefs: PreferencesManager = get()
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
installSplashScreen()
|
val vm: MainViewModel = getActivityViewModel()
|
||||||
|
|
||||||
getActivityViewModel<MainViewModel>()
|
installSplashScreen()
|
||||||
|
|
||||||
val scale = this.resources.displayMetrics.density
|
val scale = this.resources.displayMetrics.density
|
||||||
val pixels = (36 * scale).roundToInt()
|
val pixels = (36 * scale).roundToInt()
|
||||||
@ -57,8 +54,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val theme by prefs.theme.getAsState()
|
val theme by vm.prefs.theme.getAsState()
|
||||||
val dynamicColor by prefs.dynamicColor.getAsState()
|
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||||
|
|
||||||
ReVancedManagerTheme(
|
ReVancedManagerTheme(
|
||||||
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
||||||
@ -69,6 +66,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
NavBackHandler(navController)
|
NavBackHandler(navController)
|
||||||
|
|
||||||
|
val showAutoUpdatesDialog by vm.prefs.showAutoUpdatesDialog.getAsState()
|
||||||
|
if (showAutoUpdatesDialog) {
|
||||||
|
AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedNavHost(
|
AnimatedNavHost(
|
||||||
controller = navController
|
controller = navController
|
||||||
) { destination ->
|
) { destination ->
|
||||||
|
@ -3,6 +3,8 @@ package app.revanced.manager
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import app.revanced.manager.di.*
|
import app.revanced.manager.di.*
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
@ -14,6 +16,7 @@ import org.koin.core.context.startKoin
|
|||||||
class ManagerApplication : Application() {
|
class ManagerApplication : Application() {
|
||||||
private val scope = MainScope()
|
private val scope = MainScope()
|
||||||
private val prefs: PreferencesManager by inject()
|
private val prefs: PreferencesManager by inject()
|
||||||
|
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
@ -36,5 +39,11 @@ class ManagerApplication : Application() {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
prefs.preload()
|
prefs.preload()
|
||||||
}
|
}
|
||||||
|
scope.launch(Dispatchers.Default) {
|
||||||
|
with(patchBundleRepository) {
|
||||||
|
reload()
|
||||||
|
updateCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.manager.data.platform
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
|
||||||
|
class NetworkInfo(app: Application) {
|
||||||
|
private val connectivityManager = app.getSystemService<ConnectivityManager>()!!
|
||||||
|
|
||||||
|
private fun getCapabilities() = connectivityManager.activeNetwork?.let { connectivityManager.getNetworkCapabilities(it) }
|
||||||
|
fun isConnected() = connectivityManager.activeNetwork != null
|
||||||
|
fun isUnmetered() = getCapabilities()?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ?: true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if it is safe to download large files.
|
||||||
|
*/
|
||||||
|
fun isSafe() = isConnected() && isUnmetered()
|
||||||
|
}
|
@ -8,14 +8,14 @@ import app.revanced.manager.data.room.apps.DownloadedApp
|
|||||||
import app.revanced.manager.data.room.selection.PatchSelection
|
import app.revanced.manager.data.room.selection.PatchSelection
|
||||||
import app.revanced.manager.data.room.selection.SelectedPatch
|
import app.revanced.manager.data.room.selection.SelectedPatch
|
||||||
import app.revanced.manager.data.room.selection.SelectionDao
|
import app.revanced.manager.data.room.selection.SelectionDao
|
||||||
import app.revanced.manager.data.room.sources.SourceDao
|
import app.revanced.manager.data.room.bundles.PatchBundleDao
|
||||||
import app.revanced.manager.data.room.sources.SourceEntity
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Database(entities = [SourceEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class], version = 1)
|
@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class], version = 1)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun sourceDao(): SourceDao
|
abstract fun patchBundleDao(): PatchBundleDao
|
||||||
abstract fun selectionDao(): SelectionDao
|
abstract fun selectionDao(): SelectionDao
|
||||||
abstract fun appDao(): AppDao
|
abstract fun appDao(): AppDao
|
||||||
|
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
package app.revanced.manager.data.room
|
package app.revanced.manager.data.room
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import app.revanced.manager.data.room.sources.SourceLocation
|
import app.revanced.manager.data.room.bundles.Source
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class Converters {
|
class Converters {
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun locationFromString(value: String) = when(value) {
|
fun sourceFromString(value: String) = Source.from(value)
|
||||||
SourceLocation.Local.SENTINEL -> SourceLocation.Local
|
|
||||||
else -> SourceLocation.Remote(Url(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun locationToString(location: SourceLocation) = location.toString()
|
fun sourceToString(value: Source) = value.toString()
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun fileFromString(value: String) = File(value)
|
fun fileFromString(value: String) = File(value)
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
package app.revanced.manager.data.room.bundles
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface PatchBundleDao {
|
||||||
|
@Query("SELECT * FROM patch_bundles")
|
||||||
|
suspend fun all(): List<PatchBundleEntity>
|
||||||
|
|
||||||
|
@Query("SELECT version, integrations_version, auto_update FROM patch_bundles WHERE uid = :uid")
|
||||||
|
fun getPropsById(uid: Int): Flow<BundleProperties>
|
||||||
|
|
||||||
|
@Query("UPDATE patch_bundles SET version = :patches, integrations_version = :integrations WHERE uid = :uid")
|
||||||
|
suspend fun updateVersion(uid: Int, patches: String?, integrations: String?)
|
||||||
|
|
||||||
|
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
|
||||||
|
suspend fun setAutoUpdate(uid: Int, value: Boolean)
|
||||||
|
|
||||||
|
@Query("DELETE FROM patch_bundles WHERE uid != 0")
|
||||||
|
suspend fun purgeCustomBundles()
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun reset() {
|
||||||
|
purgeCustomBundles()
|
||||||
|
updateVersion(0, null, null) // Reset the main source
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
|
||||||
|
suspend fun remove(uid: Int)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun add(source: PatchBundleEntity)
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package app.revanced.manager.data.room.bundles
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
|
sealed class Source {
|
||||||
|
object Local : Source() {
|
||||||
|
const val SENTINEL = "local"
|
||||||
|
|
||||||
|
override fun toString() = SENTINEL
|
||||||
|
}
|
||||||
|
|
||||||
|
object API : Source() {
|
||||||
|
const val SENTINEL = "api"
|
||||||
|
|
||||||
|
override fun toString() = SENTINEL
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Remote(val url: Url) : Source() {
|
||||||
|
override fun toString() = url.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(value: String) = when(value) {
|
||||||
|
Local.SENTINEL -> Local
|
||||||
|
API.SENTINEL -> API
|
||||||
|
else -> Remote(Url(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class VersionInfo(
|
||||||
|
@ColumnInfo(name = "version") val patches: String? = null,
|
||||||
|
@ColumnInfo(name = "integrations_version") val integrations: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Entity(tableName = "patch_bundles", indices = [Index(value = ["name"], unique = true)])
|
||||||
|
data class PatchBundleEntity(
|
||||||
|
@PrimaryKey val uid: Int,
|
||||||
|
@ColumnInfo(name = "name") val name: String,
|
||||||
|
@Embedded val versionInfo: VersionInfo,
|
||||||
|
@ColumnInfo(name = "source") val source: Source,
|
||||||
|
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BundleProperties(
|
||||||
|
@Embedded val versionInfo: VersionInfo,
|
||||||
|
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||||
|
)
|
@ -5,20 +5,20 @@ import androidx.room.Entity
|
|||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import app.revanced.manager.data.room.sources.SourceEntity
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "patch_selections",
|
tableName = "patch_selections",
|
||||||
foreignKeys = [ForeignKey(
|
foreignKeys = [ForeignKey(
|
||||||
SourceEntity::class,
|
PatchBundleEntity::class,
|
||||||
parentColumns = ["uid"],
|
parentColumns = ["uid"],
|
||||||
childColumns = ["source"],
|
childColumns = ["patch_bundle"],
|
||||||
onDelete = ForeignKey.CASCADE
|
onDelete = ForeignKey.CASCADE
|
||||||
)],
|
)],
|
||||||
indices = [Index(value = ["source", "package_name"], unique = true)]
|
indices = [Index(value = ["patch_bundle", "package_name"], unique = true)]
|
||||||
)
|
)
|
||||||
data class PatchSelection(
|
data class PatchSelection(
|
||||||
@PrimaryKey val uid: Int,
|
@PrimaryKey val uid: Int,
|
||||||
@ColumnInfo(name = "source") val source: Int,
|
@ColumnInfo(name = "patch_bundle") val patchBundle: Int,
|
||||||
@ColumnInfo(name = "package_name") val packageName: String
|
@ColumnInfo(name = "package_name") val packageName: String
|
||||||
)
|
)
|
@ -9,9 +9,9 @@ import androidx.room.Transaction
|
|||||||
@Dao
|
@Dao
|
||||||
abstract class SelectionDao {
|
abstract class SelectionDao {
|
||||||
@Transaction
|
@Transaction
|
||||||
@MapInfo(keyColumn = "source", valueColumn = "patch_name")
|
@MapInfo(keyColumn = "patch_bundle", valueColumn = "patch_name")
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT source, patch_name FROM patch_selections" +
|
"SELECT patch_bundle, patch_name FROM patch_selections" +
|
||||||
" LEFT JOIN selected_patches ON uid = selected_patches.selection" +
|
" LEFT JOIN selected_patches ON uid = selected_patches.selection" +
|
||||||
" WHERE package_name = :packageName"
|
" WHERE package_name = :packageName"
|
||||||
)
|
)
|
||||||
@ -22,18 +22,18 @@ abstract class SelectionDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"SELECT package_name, patch_name FROM patch_selections" +
|
"SELECT package_name, patch_name FROM patch_selections" +
|
||||||
" LEFT JOIN selected_patches ON uid = selected_patches.selection" +
|
" LEFT JOIN selected_patches ON uid = selected_patches.selection" +
|
||||||
" WHERE source = :sourceUid"
|
" WHERE patch_bundle = :bundleUid"
|
||||||
)
|
)
|
||||||
abstract suspend fun exportSelection(sourceUid: Int): Map<String, List<String>>
|
abstract suspend fun exportSelection(bundleUid: Int): Map<String, List<String>>
|
||||||
|
|
||||||
@Query("SELECT uid FROM patch_selections WHERE source = :sourceUid AND package_name = :packageName")
|
@Query("SELECT uid FROM patch_selections WHERE patch_bundle = :bundleUid AND package_name = :packageName")
|
||||||
abstract suspend fun getSelectionId(sourceUid: Int, packageName: String): Int?
|
abstract suspend fun getSelectionId(bundleUid: Int, packageName: String): Int?
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
abstract suspend fun createSelection(selection: PatchSelection)
|
abstract suspend fun createSelection(selection: PatchSelection)
|
||||||
|
|
||||||
@Query("DELETE FROM patch_selections WHERE source = :uid")
|
@Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
|
||||||
abstract suspend fun clearForSource(uid: Int)
|
abstract suspend fun clearForPatchBundle(uid: Int)
|
||||||
|
|
||||||
@Query("DELETE FROM patch_selections")
|
@Query("DELETE FROM patch_selections")
|
||||||
abstract suspend fun reset()
|
abstract suspend fun reset()
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
package app.revanced.manager.data.room.sources
|
|
||||||
|
|
||||||
import androidx.room.*
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface SourceDao {
|
|
||||||
@Query("SELECT * FROM $sourcesTableName")
|
|
||||||
suspend fun all(): List<SourceEntity>
|
|
||||||
|
|
||||||
@Query("SELECT version, integrations_version FROM $sourcesTableName WHERE uid = :uid")
|
|
||||||
suspend fun getVersionById(uid: Int): VersionInfo
|
|
||||||
|
|
||||||
@Query("UPDATE $sourcesTableName SET version=:patches, integrations_version=:integrations WHERE uid=:uid")
|
|
||||||
suspend fun updateVersion(uid: Int, patches: String, integrations: String)
|
|
||||||
|
|
||||||
@Query("DELETE FROM $sourcesTableName")
|
|
||||||
suspend fun purge()
|
|
||||||
|
|
||||||
@Query("DELETE FROM $sourcesTableName WHERE uid=:uid")
|
|
||||||
suspend fun remove(uid: Int)
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
suspend fun add(source: SourceEntity)
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
package app.revanced.manager.data.room.sources
|
|
||||||
|
|
||||||
import androidx.room.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
|
|
||||||
const val sourcesTableName = "sources"
|
|
||||||
|
|
||||||
sealed class SourceLocation {
|
|
||||||
object Local : SourceLocation() {
|
|
||||||
const val SENTINEL = "local"
|
|
||||||
|
|
||||||
override fun toString() = SENTINEL
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Remote(val url: Url) : SourceLocation() {
|
|
||||||
override fun toString() = url.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class VersionInfo(
|
|
||||||
@ColumnInfo(name = "version") val patches: String,
|
|
||||||
@ColumnInfo(name = "integrations_version") val integrations: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Entity(tableName = sourcesTableName, indices = [Index(value = ["name"], unique = true)])
|
|
||||||
data class SourceEntity(
|
|
||||||
@PrimaryKey val uid: Int,
|
|
||||||
@ColumnInfo(name = "name") val name: String,
|
|
||||||
@Embedded val versionInfo: VersionInfo,
|
|
||||||
@ColumnInfo(name = "location") val location: SourceLocation,
|
|
||||||
)
|
|
@ -1,6 +1,7 @@
|
|||||||
package app.revanced.manager.di
|
package app.revanced.manager.di
|
||||||
|
|
||||||
import app.revanced.manager.data.platform.FileSystem
|
import app.revanced.manager.data.platform.FileSystem
|
||||||
|
import app.revanced.manager.data.platform.NetworkInfo
|
||||||
import app.revanced.manager.domain.repository.*
|
import app.revanced.manager.domain.repository.*
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
import app.revanced.manager.network.api.ManagerAPI
|
import app.revanced.manager.network.api.ManagerAPI
|
||||||
@ -12,9 +13,10 @@ val repositoryModule = module {
|
|||||||
singleOf(::GithubRepository)
|
singleOf(::GithubRepository)
|
||||||
singleOf(::ManagerAPI)
|
singleOf(::ManagerAPI)
|
||||||
singleOf(::FileSystem)
|
singleOf(::FileSystem)
|
||||||
singleOf(::SourcePersistenceRepository)
|
singleOf(::NetworkInfo)
|
||||||
|
singleOf(::PatchBundlePersistenceRepository)
|
||||||
singleOf(::PatchSelectionRepository)
|
singleOf(::PatchSelectionRepository)
|
||||||
singleOf(::SourceRepository)
|
singleOf(::PatchBundleRepository)
|
||||||
singleOf(::WorkerRepository)
|
singleOf(::WorkerRepository)
|
||||||
singleOf(::DownloadedAppRepository)
|
singleOf(::DownloadedAppRepository)
|
||||||
}
|
}
|
@ -6,11 +6,13 @@ import org.koin.dsl.module
|
|||||||
|
|
||||||
val viewModelModule = module {
|
val viewModelModule = module {
|
||||||
viewModelOf(::MainViewModel)
|
viewModelOf(::MainViewModel)
|
||||||
|
viewModelOf(::DashboardViewModel)
|
||||||
viewModelOf(::PatchesSelectorViewModel)
|
viewModelOf(::PatchesSelectorViewModel)
|
||||||
viewModelOf(::SettingsViewModel)
|
viewModelOf(::SettingsViewModel)
|
||||||
|
viewModelOf(::AdvancedSettingsViewModel)
|
||||||
viewModelOf(::AppSelectorViewModel)
|
viewModelOf(::AppSelectorViewModel)
|
||||||
viewModelOf(::VersionSelectorViewModel)
|
viewModelOf(::VersionSelectorViewModel)
|
||||||
viewModelOf(::SourcesViewModel)
|
viewModelOf(::BundlesViewModel)
|
||||||
viewModelOf(::InstallerViewModel)
|
viewModelOf(::InstallerViewModel)
|
||||||
viewModelOf(::UpdateProgressViewModel)
|
viewModelOf(::UpdateProgressViewModel)
|
||||||
viewModelOf(::ManagerUpdateChangelogViewModel)
|
viewModelOf(::ManagerUpdateChangelogViewModel)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package app.revanced.manager.domain.sources
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -7,17 +7,17 @@ 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 LocalSource(name: String, id: Int, directory: File) : Source(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 {
|
patches?.let {
|
||||||
Files.copy(it, patchesJar.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
Files.copy(it, patchesFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
}
|
}
|
||||||
integrations?.let {
|
integrations?.let {
|
||||||
Files.copy(it, this@LocalSource.integrations.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
Files.copy(it, this@LocalPatchBundle.integrationsFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_bundle.emit(loadBundle { throw it })
|
reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchBundle] source.
|
||||||
|
*/
|
||||||
|
@Stable
|
||||||
|
sealed class PatchBundleSource(val name: String, val uid: Int, directory: File) {
|
||||||
|
protected val patchesFile = directory.resolve("patches.jar")
|
||||||
|
protected val integrationsFile = directory.resolve("integrations.apk")
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(load())
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the bundle has been downloaded to local storage.
|
||||||
|
*/
|
||||||
|
fun hasInstalled() = patchesFile.exists()
|
||||||
|
|
||||||
|
private fun load(): State {
|
||||||
|
if (!hasInstalled()) return State.Missing
|
||||||
|
|
||||||
|
return try {
|
||||||
|
State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
State.Failed(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reload() {
|
||||||
|
_state.value = load()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface State {
|
||||||
|
fun patchBundleOrNull(): PatchBundle? = null
|
||||||
|
|
||||||
|
object Missing : State
|
||||||
|
data class Failed(val throwable: Throwable) : State
|
||||||
|
data class Loaded(val bundle: PatchBundle) : State {
|
||||||
|
override fun patchBundleOrNull() = bundle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val PatchBundleSource.isDefault get() = uid == 0
|
||||||
|
val PatchBundleSource.asRemoteOrNull get() = this as? RemotePatchBundle
|
||||||
|
fun PatchBundleSource.propsOrNullFlow() = asRemoteOrNull?.propsFlow() ?: flowOf(null)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import app.revanced.manager.data.room.bundles.VersionInfo
|
||||||
|
import app.revanced.manager.domain.bundles.APIPatchBundle.Companion.toBundleAsset
|
||||||
|
import app.revanced.manager.domain.repository.Assets
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
|
||||||
|
import app.revanced.manager.domain.repository.ReVancedRepository
|
||||||
|
import app.revanced.manager.network.dto.Asset
|
||||||
|
import app.revanced.manager.network.dto.BundleAsset
|
||||||
|
import app.revanced.manager.network.dto.BundleInfo
|
||||||
|
import app.revanced.manager.network.service.HttpService
|
||||||
|
import app.revanced.manager.network.utils.getOrThrow
|
||||||
|
import app.revanced.manager.util.ghIntegrations
|
||||||
|
import app.revanced.manager.util.ghPatches
|
||||||
|
import io.ktor.client.request.url
|
||||||
|
import io.ktor.http.Url
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) :
|
||||||
|
PatchBundleSource(name, id, directory), KoinComponent {
|
||||||
|
private val configRepository: PatchBundlePersistenceRepository by inject()
|
||||||
|
protected val http: HttpService by inject()
|
||||||
|
|
||||||
|
protected abstract suspend fun getLatestInfo(): BundleInfo
|
||||||
|
|
||||||
|
private suspend fun download(info: BundleInfo) = withContext(Dispatchers.IO) {
|
||||||
|
val (patches, integrations) = info
|
||||||
|
coroutineScope {
|
||||||
|
mapOf(
|
||||||
|
patches.url to patchesFile,
|
||||||
|
integrations.url to integrationsFile
|
||||||
|
).forEach { (asset, file) ->
|
||||||
|
launch {
|
||||||
|
http.download(file) {
|
||||||
|
url(asset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveVersion(patches.version, integrations.version)
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadLatest() {
|
||||||
|
download(getLatestInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
val info = getLatestInfo()
|
||||||
|
if (hasInstalled() && VersionInfo(info.patches.version, info.integrations.version) == currentVersion()) {
|
||||||
|
return@withContext false
|
||||||
|
}
|
||||||
|
|
||||||
|
download(info)
|
||||||
|
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) {
|
||||||
|
arrayOf(patchesFile, integrationsFile).forEach(File::delete)
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun propsFlow() = configRepository.getProps(uid)
|
||||||
|
|
||||||
|
suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val updateFailMsg = "Failed to update patch bundle(s)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
||||||
|
RemotePatchBundle(name, id, directory, endpoint) {
|
||||||
|
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
|
||||||
|
http.request<BundleInfo> {
|
||||||
|
url(endpoint)
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
||||||
|
RemotePatchBundle(name, id, directory, endpoint) {
|
||||||
|
private val api: ReVancedRepository by inject()
|
||||||
|
|
||||||
|
override suspend fun getLatestInfo() = api.getAssets().toBundleInfo()
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
fun Assets.toBundleInfo(): BundleInfo {
|
||||||
|
val patches = find(ghPatches, ".jar")
|
||||||
|
val integrations = find(ghIntegrations, ".apk")
|
||||||
|
|
||||||
|
return BundleInfo(patches.toBundleAsset(), integrations.toBundleAsset())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Asset.toBundleAsset() = BundleAsset(version, downloadUrl)
|
||||||
|
}
|
||||||
|
}
|
@ -10,10 +10,15 @@ class PreferencesManager(
|
|||||||
val dynamicColor = booleanPreference("dynamic_color", true)
|
val dynamicColor = booleanPreference("dynamic_color", true)
|
||||||
val theme = enumPreference("theme", Theme.SYSTEM)
|
val theme = enumPreference("theme", Theme.SYSTEM)
|
||||||
|
|
||||||
|
val api = stringPreference("api_url", "https://releases.revanced.app")
|
||||||
|
|
||||||
val allowExperimental = booleanPreference("allow_experimental", false)
|
val allowExperimental = booleanPreference("allow_experimental", false)
|
||||||
|
|
||||||
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
||||||
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
||||||
|
|
||||||
val preferSplits = booleanPreference("prefer_splits", false)
|
val preferSplits = booleanPreference("prefer_splits", false)
|
||||||
|
|
||||||
|
val showAutoUpdatesDialog = booleanPreference("show_auto_updates_dialog", true)
|
||||||
|
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
import app.revanced.manager.data.room.bundles.Source
|
||||||
|
import app.revanced.manager.data.room.bundles.VersionInfo
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
|
class PatchBundlePersistenceRepository(db: AppDatabase) {
|
||||||
|
private val dao = db.patchBundleDao()
|
||||||
|
|
||||||
|
suspend fun loadConfiguration(): List<PatchBundleEntity> {
|
||||||
|
val all = dao.all()
|
||||||
|
if (all.isEmpty()) {
|
||||||
|
dao.add(defaultSource)
|
||||||
|
return listOf(defaultSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reset() = dao.reset()
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) =
|
||||||
|
PatchBundleEntity(
|
||||||
|
uid = generateUid(),
|
||||||
|
name = name,
|
||||||
|
versionInfo = VersionInfo(),
|
||||||
|
source = source,
|
||||||
|
autoUpdate = autoUpdate
|
||||||
|
).also {
|
||||||
|
dao.add(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(uid: Int) = dao.remove(uid)
|
||||||
|
|
||||||
|
suspend fun updateVersion(uid: Int, patches: String, integrations: String) =
|
||||||
|
dao.updateVersion(uid, patches, integrations)
|
||||||
|
|
||||||
|
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
|
||||||
|
|
||||||
|
fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged()
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val defaultSource = PatchBundleEntity(
|
||||||
|
uid = 0,
|
||||||
|
name = "Main",
|
||||||
|
versionInfo = VersionInfo(),
|
||||||
|
source = Source.API,
|
||||||
|
autoUpdate = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.data.platform.NetworkInfo
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
import app.revanced.manager.domain.bundles.APIPatchBundle
|
||||||
|
import app.revanced.manager.domain.bundles.JsonPatchBundle
|
||||||
|
import app.revanced.manager.data.room.bundles.Source as SourceInfo
|
||||||
|
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
||||||
|
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||||
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.util.flatMapLatestAndCombine
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class PatchBundleRepository(
|
||||||
|
app: Application,
|
||||||
|
private val persistenceRepo: PatchBundlePersistenceRepository,
|
||||||
|
private val networkInfo: NetworkInfo,
|
||||||
|
) {
|
||||||
|
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
private val _sources: MutableStateFlow<Map<Int, PatchBundleSource>> =
|
||||||
|
MutableStateFlow(emptyMap())
|
||||||
|
val sources = _sources.map { it.values.toList() }
|
||||||
|
|
||||||
|
val bundles = sources.flatMapLatestAndCombine(
|
||||||
|
combiner = {
|
||||||
|
it.mapNotNull { (uid, state) ->
|
||||||
|
val bundle = state.patchBundleOrNull() ?: return@mapNotNull null
|
||||||
|
uid to bundle
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
it.state.map { state -> it.uid to state }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the directory of the [PatchBundleSource] with the specified [uid], creating it if needed.
|
||||||
|
*/
|
||||||
|
private fun directoryOf(uid: Int) = bundlesDir.resolve(uid.toString()).also { it.mkdirs() }
|
||||||
|
|
||||||
|
private fun PatchBundleEntity.load(): PatchBundleSource {
|
||||||
|
val dir = directoryOf(uid)
|
||||||
|
|
||||||
|
return when (source) {
|
||||||
|
is SourceInfo.Local -> LocalPatchBundle(name, uid, dir)
|
||||||
|
is SourceInfo.API -> APIPatchBundle(name, uid, dir, SourceInfo.API.SENTINEL)
|
||||||
|
is SourceInfo.Remote -> JsonPatchBundle(
|
||||||
|
name,
|
||||||
|
uid,
|
||||||
|
dir,
|
||||||
|
source.url.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reload() = withContext(Dispatchers.Default) {
|
||||||
|
val entities = persistenceRepo.loadConfiguration().onEach {
|
||||||
|
Log.d(tag, "Bundle: $it")
|
||||||
|
}
|
||||||
|
|
||||||
|
_sources.value = entities.associate {
|
||||||
|
it.uid to it.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reset() = withContext(Dispatchers.Default) {
|
||||||
|
persistenceRepo.reset()
|
||||||
|
_sources.value = emptyMap()
|
||||||
|
bundlesDir.apply {
|
||||||
|
deleteRecursively()
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun remove(bundle: PatchBundleSource) = withContext(Dispatchers.Default) {
|
||||||
|
persistenceRepo.delete(bundle.uid)
|
||||||
|
directoryOf(bundle.uid).deleteRecursively()
|
||||||
|
|
||||||
|
_sources.update {
|
||||||
|
it.filterKeys { key ->
|
||||||
|
key != bundle.uid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addBundle(patchBundle: PatchBundleSource) =
|
||||||
|
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
|
||||||
|
|
||||||
|
suspend fun createLocal(name: String, patches: InputStream, integrations: InputStream?) {
|
||||||
|
val id = persistenceRepo.create(name, SourceInfo.Local).uid
|
||||||
|
val bundle = LocalPatchBundle(name, id, directoryOf(id))
|
||||||
|
|
||||||
|
bundle.replace(patches, integrations)
|
||||||
|
addBundle(bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createRemote(name: String, url: String, autoUpdate: Boolean) {
|
||||||
|
val entity = persistenceRepo.create(name, SourceInfo.from(url), autoUpdate)
|
||||||
|
addBundle(entity.load())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <reified T> getBundlesByType() =
|
||||||
|
sources.first().filterIsInstance<T>()
|
||||||
|
|
||||||
|
suspend fun reloadApiBundles() {
|
||||||
|
getBundlesByType<APIPatchBundle>().forEach {
|
||||||
|
it.deleteLocalFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun redownloadRemoteBundles() = getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() }
|
||||||
|
|
||||||
|
suspend fun updateCheck() = supervisorScope {
|
||||||
|
if (!networkInfo.isSafe()) {
|
||||||
|
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||||
|
return@supervisorScope
|
||||||
|
}
|
||||||
|
|
||||||
|
getBundlesByType<RemotePatchBundle>().forEach {
|
||||||
|
launch {
|
||||||
|
if (!it.propsFlow().first().autoUpdate) return@launch
|
||||||
|
Log.d(tag, "Updating patch bundle: ${it.name}")
|
||||||
|
it.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,15 +3,14 @@ package app.revanced.manager.domain.repository
|
|||||||
import app.revanced.manager.data.room.AppDatabase
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||||
import app.revanced.manager.data.room.selection.PatchSelection
|
import app.revanced.manager.data.room.selection.PatchSelection
|
||||||
import app.revanced.manager.domain.sources.Source
|
|
||||||
|
|
||||||
class PatchSelectionRepository(db: AppDatabase) {
|
class PatchSelectionRepository(db: AppDatabase) {
|
||||||
private val dao = db.selectionDao()
|
private val dao = db.selectionDao()
|
||||||
|
|
||||||
private suspend fun getOrCreateSelection(sourceUid: Int, packageName: String) =
|
private suspend fun getOrCreateSelection(bundleUid: Int, packageName: String) =
|
||||||
dao.getSelectionId(sourceUid, packageName) ?: PatchSelection(
|
dao.getSelectionId(bundleUid, packageName) ?: PatchSelection(
|
||||||
uid = generateUid(),
|
uid = generateUid(),
|
||||||
source = sourceUid,
|
patchBundle = bundleUid,
|
||||||
packageName = packageName
|
packageName = packageName
|
||||||
).also { dao.createSelection(it) }.uid
|
).also { dao.createSelection(it) }.uid
|
||||||
|
|
||||||
@ -28,12 +27,12 @@ class PatchSelectionRepository(db: AppDatabase) {
|
|||||||
|
|
||||||
suspend fun reset() = dao.reset()
|
suspend fun reset() = dao.reset()
|
||||||
|
|
||||||
suspend fun export(source: Source): SerializedSelection = dao.exportSelection(source.uid)
|
suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid)
|
||||||
|
|
||||||
suspend fun import(source: Source, selection: SerializedSelection) {
|
suspend fun import(bundleUid: Int, selection: SerializedSelection) {
|
||||||
dao.clearForSource(source.uid)
|
dao.clearForPatchBundle(bundleUid)
|
||||||
dao.updateSelections(selection.entries.associate { (packageName, patches) ->
|
dao.updateSelections(selection.entries.associate { (packageName, patches) ->
|
||||||
getOrCreateSelection(source.uid, packageName) to patches.toSet()
|
getOrCreateSelection(bundleUid, packageName) to patches.toSet()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,25 @@
|
|||||||
package app.revanced.manager.domain.repository
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.network.api.MissingAssetException
|
||||||
|
import app.revanced.manager.network.dto.Asset
|
||||||
|
import app.revanced.manager.network.dto.ReVancedReleases
|
||||||
import app.revanced.manager.network.service.ReVancedService
|
import app.revanced.manager.network.service.ReVancedService
|
||||||
|
import app.revanced.manager.network.utils.getOrThrow
|
||||||
|
|
||||||
class ReVancedRepository(
|
class ReVancedRepository(
|
||||||
private val service: ReVancedService
|
private val service: ReVancedService,
|
||||||
|
private val prefs: PreferencesManager
|
||||||
) {
|
) {
|
||||||
suspend fun getAssets() = service.getAssets()
|
private suspend fun apiUrl() = prefs.api.get()
|
||||||
|
|
||||||
suspend fun getContributors() = service.getContributors()
|
suspend fun getContributors() = service.getContributors(apiUrl())
|
||||||
|
|
||||||
suspend fun findAsset(repo: String, file: String) = service.findAsset(repo, file)
|
suspend fun getAssets() = Assets(service.getAssets(apiUrl()).getOrThrow())
|
||||||
|
}
|
||||||
|
|
||||||
|
class Assets(private val releases: ReVancedReleases): List<Asset> by releases.tools {
|
||||||
|
fun find(repo: String, file: String) = find { asset ->
|
||||||
|
asset.name.contains(file) && asset.repository.contains(repo)
|
||||||
|
} ?: throw MissingAssetException()
|
||||||
}
|
}
|
@ -1,55 +0,0 @@
|
|||||||
package app.revanced.manager.domain.repository
|
|
||||||
|
|
||||||
import app.revanced.manager.data.room.AppDatabase
|
|
||||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
|
||||||
import app.revanced.manager.data.room.sources.SourceEntity
|
|
||||||
import app.revanced.manager.data.room.sources.SourceLocation
|
|
||||||
import app.revanced.manager.data.room.sources.VersionInfo
|
|
||||||
import app.revanced.manager.util.apiURL
|
|
||||||
import io.ktor.http.*
|
|
||||||
|
|
||||||
class SourcePersistenceRepository(db: AppDatabase) {
|
|
||||||
private val dao = db.sourceDao()
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
val defaultSource = SourceEntity(
|
|
||||||
uid = generateUid(),
|
|
||||||
name = "Official",
|
|
||||||
versionInfo = VersionInfo("", ""),
|
|
||||||
location = SourceLocation.Remote(Url(apiURL))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun loadConfiguration(): List<SourceEntity> {
|
|
||||||
val all = dao.all()
|
|
||||||
if (all.isEmpty()) {
|
|
||||||
dao.add(defaultSource)
|
|
||||||
return listOf(defaultSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
return all
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun clear() = dao.purge()
|
|
||||||
|
|
||||||
suspend fun create(name: String, location: SourceLocation): Int {
|
|
||||||
val uid = generateUid()
|
|
||||||
dao.add(
|
|
||||||
SourceEntity(
|
|
||||||
uid = uid,
|
|
||||||
name = name,
|
|
||||||
versionInfo = VersionInfo("", ""),
|
|
||||||
location = location,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return uid
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun delete(uid: Int) = dao.remove(uid)
|
|
||||||
|
|
||||||
suspend fun updateVersion(uid: Int, patches: String, integrations: String) =
|
|
||||||
dao.updateVersion(uid, patches, integrations)
|
|
||||||
|
|
||||||
suspend fun getVersion(id: Int) = dao.getVersionById(id).let { it.patches to it.integrations }
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
package app.revanced.manager.domain.repository
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import app.revanced.manager.data.room.sources.SourceEntity
|
|
||||||
import app.revanced.manager.data.room.sources.SourceLocation
|
|
||||||
import app.revanced.manager.domain.sources.LocalSource
|
|
||||||
import app.revanced.manager.domain.sources.RemoteSource
|
|
||||||
import app.revanced.manager.domain.sources.Source
|
|
||||||
import app.revanced.manager.util.flatMapLatestAndCombine
|
|
||||||
import app.revanced.manager.util.tag
|
|
||||||
import io.ktor.http.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
class SourceRepository(app: Application, private val persistenceRepo: SourcePersistenceRepository) {
|
|
||||||
private val sourcesDir = app.getDir("sources", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
private val _sources: MutableStateFlow<Map<Int, Source>> = MutableStateFlow(emptyMap())
|
|
||||||
val sources = _sources.map { it.values.toList() }
|
|
||||||
|
|
||||||
val bundles = sources.flatMapLatestAndCombine(
|
|
||||||
combiner = { it.toMap() }
|
|
||||||
) {
|
|
||||||
it.bundle.map { bundle -> it.uid to bundle }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the directory of the [Source] with the specified [uid], creating it if needed.
|
|
||||||
*/
|
|
||||||
private fun directoryOf(uid: Int) = sourcesDir.resolve(uid.toString()).also { it.mkdirs() }
|
|
||||||
|
|
||||||
private fun SourceEntity.load(dir: File) = when (location) {
|
|
||||||
is SourceLocation.Local -> LocalSource(name, uid, dir)
|
|
||||||
is SourceLocation.Remote -> RemoteSource(name, uid, dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun loadSources() = withContext(Dispatchers.Default) {
|
|
||||||
val sourcesConfig = persistenceRepo.loadConfiguration().onEach {
|
|
||||||
Log.d(tag, "Source: $it")
|
|
||||||
}
|
|
||||||
|
|
||||||
val sources = sourcesConfig.associate {
|
|
||||||
val dir = directoryOf(it.uid)
|
|
||||||
val source = it.load(dir)
|
|
||||||
|
|
||||||
it.uid to source
|
|
||||||
}
|
|
||||||
|
|
||||||
_sources.emit(sources)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun resetConfig() = withContext(Dispatchers.Default) {
|
|
||||||
persistenceRepo.clear()
|
|
||||||
_sources.emit(emptyMap())
|
|
||||||
sourcesDir.apply {
|
|
||||||
deleteRecursively()
|
|
||||||
mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSources()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun remove(source: Source) = withContext(Dispatchers.Default) {
|
|
||||||
persistenceRepo.delete(source.uid)
|
|
||||||
directoryOf(source.uid).deleteRecursively()
|
|
||||||
|
|
||||||
_sources.update {
|
|
||||||
it.filterValues { value ->
|
|
||||||
value.uid != source.uid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addSource(source: Source) =
|
|
||||||
_sources.update { it.toMutableMap().apply { put(source.uid, source) } }
|
|
||||||
|
|
||||||
suspend fun createLocalSource(name: String, patches: InputStream, integrations: InputStream?) {
|
|
||||||
val id = persistenceRepo.create(name, SourceLocation.Local)
|
|
||||||
val source = LocalSource(name, id, directoryOf(id))
|
|
||||||
|
|
||||||
addSource(source)
|
|
||||||
|
|
||||||
source.replace(patches, integrations)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun createRemoteSource(name: String, apiUrl: Url) {
|
|
||||||
val id = persistenceRepo.create(name, SourceLocation.Remote(apiUrl))
|
|
||||||
addSource(RemoteSource(name, id, directoryOf(id)))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun redownloadRemoteSources() =
|
|
||||||
sources.first().filterIsInstance<RemoteSource>().forEach { it.downloadLatest() }
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package app.revanced.manager.domain.sources
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
|
||||||
import app.revanced.manager.network.api.ManagerAPI
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.core.component.get
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@Stable
|
|
||||||
class RemoteSource(name: String, id: Int, directory: File) : Source(name, id, directory) {
|
|
||||||
private val api: ManagerAPI = get()
|
|
||||||
suspend fun downloadLatest() = withContext(Dispatchers.IO) {
|
|
||||||
api.downloadBundle(patchesJar, integrations).also { (patchesVer, integrationsVer) ->
|
|
||||||
saveVersion(patchesVer, integrationsVer)
|
|
||||||
_bundle.emit(loadBundle { err -> throw err })
|
|
||||||
}
|
|
||||||
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun update() = withContext(Dispatchers.IO) {
|
|
||||||
val currentVersion = getVersion()
|
|
||||||
if (!hasInstalled() || currentVersion != api.getLatestBundleVersion()) {
|
|
||||||
downloadLatest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
package app.revanced.manager.domain.sources
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.Stable
|
|
||||||
import app.revanced.manager.patcher.patch.PatchBundle
|
|
||||||
import app.revanced.manager.domain.repository.SourcePersistenceRepository
|
|
||||||
import app.revanced.manager.util.tag
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
import org.koin.core.component.inject
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [PatchBundle] source.
|
|
||||||
*/
|
|
||||||
@Stable
|
|
||||||
sealed class Source(val name: String, val uid: Int, directory: File) : KoinComponent {
|
|
||||||
private val configRepository: SourcePersistenceRepository by inject()
|
|
||||||
protected companion object {
|
|
||||||
/**
|
|
||||||
* A placeholder [PatchBundle].
|
|
||||||
*/
|
|
||||||
val emptyPatchBundle = PatchBundle(emptyList(), null)
|
|
||||||
fun logError(err: Throwable) {
|
|
||||||
Log.e(tag, "Failed to load bundle", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected val patchesJar = directory.resolve("patches.jar")
|
|
||||||
protected val integrations = directory.resolve("integrations.apk")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the bundle has been downloaded to local storage.
|
|
||||||
*/
|
|
||||||
fun hasInstalled() = patchesJar.exists()
|
|
||||||
|
|
||||||
protected suspend fun getVersion() = configRepository.getVersion(uid)
|
|
||||||
protected suspend fun saveVersion(patches: String, integrations: String) =
|
|
||||||
configRepository.updateVersion(uid, patches, integrations)
|
|
||||||
|
|
||||||
// TODO: Communicate failure states better.
|
|
||||||
protected fun loadBundle(onFail: (Throwable) -> Unit = ::logError) = if (!hasInstalled()) emptyPatchBundle
|
|
||||||
else try {
|
|
||||||
PatchBundle(patchesJar, integrations.takeIf { it.exists() })
|
|
||||||
} catch (err: Throwable) {
|
|
||||||
onFail(err)
|
|
||||||
emptyPatchBundle
|
|
||||||
}
|
|
||||||
|
|
||||||
protected val _bundle = MutableStateFlow(loadBundle())
|
|
||||||
val bundle = _bundle.asStateFlow()
|
|
||||||
}
|
|
@ -1,59 +1,43 @@
|
|||||||
package app.revanced.manager.network.api
|
package app.revanced.manager.network.api
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.os.Environment
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import app.revanced.manager.domain.repository.Assets
|
||||||
import app.revanced.manager.domain.repository.ReVancedRepository
|
import app.revanced.manager.domain.repository.ReVancedRepository
|
||||||
|
import app.revanced.manager.network.dto.Asset
|
||||||
|
import app.revanced.manager.network.service.HttpService
|
||||||
import app.revanced.manager.util.*
|
import app.revanced.manager.util.*
|
||||||
import io.ktor.client.*
|
import io.ktor.client.plugins.onDownload
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.request.url
|
||||||
import io.ktor.client.request.*
|
|
||||||
import io.ktor.client.statement.*
|
|
||||||
import io.ktor.util.cio.*
|
|
||||||
import io.ktor.utils.io.*
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
// TODO: merge ReVancedRepository into this class
|
||||||
class ManagerAPI(
|
class ManagerAPI(
|
||||||
private val client: HttpClient,
|
private val http: HttpService,
|
||||||
private val revancedRepository: ReVancedRepository
|
private val revancedRepository: ReVancedRepository
|
||||||
) {
|
) {
|
||||||
var downloadProgress: Float? by mutableStateOf(null)
|
var downloadProgress: Float? by mutableStateOf(null)
|
||||||
var downloadedSize: Long? by mutableStateOf(null)
|
var downloadedSize: Long? by mutableStateOf(null)
|
||||||
var totalSize: Long? by mutableStateOf(null)
|
var totalSize: Long? by mutableStateOf(null)
|
||||||
|
|
||||||
private suspend fun downloadAsset(downloadUrl: String, saveLocation: File) {
|
private suspend fun downloadAsset(asset: Asset, saveLocation: File) {
|
||||||
client.get(downloadUrl) {
|
http.download(saveLocation) {
|
||||||
|
url(asset.downloadUrl)
|
||||||
onDownload { bytesSentTotal, contentLength ->
|
onDownload { bytesSentTotal, contentLength ->
|
||||||
downloadProgress = (bytesSentTotal.toFloat() / contentLength.toFloat())
|
downloadProgress = (bytesSentTotal.toFloat() / contentLength.toFloat())
|
||||||
downloadedSize = bytesSentTotal
|
downloadedSize = bytesSentTotal
|
||||||
totalSize = contentLength
|
totalSize = contentLength
|
||||||
}
|
}
|
||||||
}.bodyAsChannel().copyAndClose(saveLocation.writeChannel())
|
}
|
||||||
downloadProgress = null
|
downloadProgress = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun patchesAsset() = revancedRepository.findAsset(ghPatches, ".jar")
|
|
||||||
private suspend fun integrationsAsset() = revancedRepository.findAsset(ghIntegrations, ".apk")
|
|
||||||
|
|
||||||
suspend fun getLatestBundleVersion() = patchesAsset().version to integrationsAsset().version
|
|
||||||
|
|
||||||
suspend fun downloadBundle(patchBundle: File, integrations: File): Pair<String, String> {
|
|
||||||
val patchBundleAsset = patchesAsset()
|
|
||||||
val integrationsAsset = integrationsAsset()
|
|
||||||
|
|
||||||
downloadAsset(patchBundleAsset.downloadUrl, patchBundle)
|
|
||||||
downloadAsset(integrationsAsset.downloadUrl, integrations)
|
|
||||||
|
|
||||||
return patchBundleAsset.version to integrationsAsset.version
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun downloadManager(location: File) {
|
suspend fun downloadManager(location: File) {
|
||||||
val managerAsset = revancedRepository.findAsset(ghManager, ".apk")
|
val managerAsset = revancedRepository.getAssets().find(ghManager, ".apk")
|
||||||
downloadAsset(managerAsset.downloadUrl, location)
|
downloadAsset(managerAsset, location)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MissingAssetException : Exception()
|
class MissingAssetException : Exception()
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.manager.network.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BundleInfo(val patches: BundleAsset, val integrations: BundleAsset)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BundleAsset(val version: String, val url: String)
|
@ -6,7 +6,6 @@ import app.revanced.manager.network.dto.ReVancedReleases
|
|||||||
import app.revanced.manager.network.dto.ReVancedRepositories
|
import app.revanced.manager.network.dto.ReVancedRepositories
|
||||||
import app.revanced.manager.network.utils.APIResponse
|
import app.revanced.manager.network.utils.APIResponse
|
||||||
import app.revanced.manager.network.utils.getOrThrow
|
import app.revanced.manager.network.utils.getOrThrow
|
||||||
import app.revanced.manager.util.apiURL
|
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -14,33 +13,20 @@ import kotlinx.coroutines.withContext
|
|||||||
class ReVancedService(
|
class ReVancedService(
|
||||||
private val client: HttpService,
|
private val client: HttpService,
|
||||||
) {
|
) {
|
||||||
suspend fun getAssets(): APIResponse<ReVancedReleases> {
|
suspend fun getAssets(api: String): APIResponse<ReVancedReleases> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
client.request {
|
client.request {
|
||||||
url("$apiUrl/tools")
|
url("$api/tools")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getContributors(): APIResponse<ReVancedRepositories> {
|
suspend fun getContributors(api: String): APIResponse<ReVancedRepositories> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
client.request {
|
client.request {
|
||||||
url("$apiUrl/contributors")
|
url("$api/contributors")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findAsset(repo: String, file: String): Asset {
|
|
||||||
val releases = getAssets().getOrThrow()
|
|
||||||
|
|
||||||
val asset = releases.tools.find { asset ->
|
|
||||||
(asset.name.contains(file) && asset.repository.contains(repo))
|
|
||||||
} ?: throw MissingAssetException()
|
|
||||||
|
|
||||||
return asset
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
private const val apiUrl = apiURL
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -16,7 +16,7 @@ import androidx.work.WorkerParameters
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||||
import app.revanced.manager.domain.repository.SourceRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.domain.worker.Worker
|
import app.revanced.manager.domain.worker.Worker
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
import app.revanced.manager.patcher.Session
|
import app.revanced.manager.patcher.Session
|
||||||
@ -43,7 +43,7 @@ class PatcherWorker(
|
|||||||
parameters: WorkerParameters
|
parameters: WorkerParameters
|
||||||
) : Worker<PatcherWorker.Args>(context, parameters), KoinComponent {
|
) : Worker<PatcherWorker.Args>(context, parameters), KoinComponent {
|
||||||
|
|
||||||
private val sourceRepository: SourceRepository by inject()
|
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||||
private val workerRepository: WorkerRepository by inject()
|
private val workerRepository: WorkerRepository by inject()
|
||||||
private val prefs: PreferencesManager by inject()
|
private val prefs: PreferencesManager by inject()
|
||||||
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
||||||
@ -124,7 +124,7 @@ class PatcherWorker(
|
|||||||
val frameworkPath =
|
val frameworkPath =
|
||||||
applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
||||||
|
|
||||||
val bundles = sourceRepository.bundles.first()
|
val bundles = patchBundleRepository.bundles.first()
|
||||||
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
|
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
|
||||||
|
|
||||||
val downloadProgress = MutableStateFlow<Pair<Float, Float>?>(null)
|
val downloadProgress = MutableStateFlow<Pair<Float, Float>?>(null)
|
||||||
|
@ -0,0 +1,115 @@
|
|||||||
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Source
|
||||||
|
import androidx.compose.material.icons.outlined.Update
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.revanced.manager.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AutoUpdatesDialog(onSubmit: (Boolean, Boolean) -> Unit) {
|
||||||
|
var patchesEnabled by rememberSaveable { mutableStateOf(true) }
|
||||||
|
var managerEnabled by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = { onSubmit(managerEnabled, patchesEnabled) }
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.save))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(Icons.Outlined.Update, null)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.auto_updates_dialog_title),
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.auto_updates_dialog_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
|
||||||
|
AutoUpdatesItem(
|
||||||
|
headline = R.string.auto_updates_dialog_manager,
|
||||||
|
icon = Icons.Outlined.Update,
|
||||||
|
checked = managerEnabled,
|
||||||
|
onCheckedChange = { managerEnabled = it }
|
||||||
|
)
|
||||||
|
Divider()
|
||||||
|
AutoUpdatesItem(
|
||||||
|
headline = R.string.auto_updates_dialog_patches,
|
||||||
|
icon = Icons.Outlined.Source,
|
||||||
|
checked = patchesEnabled,
|
||||||
|
onCheckedChange = { patchesEnabled = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.auto_updates_dialog_note),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AutoUpdatesItem(
|
||||||
|
@StringRes headline: Int,
|
||||||
|
icon: ImageVector,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = { Icon(icon, null, tint = MaterialTheme.colorScheme.onSurface) },
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(headline),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Checkbox(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable { onCheckedChange(!checked) }
|
||||||
|
)
|
||||||
|
}
|
@ -1,96 +0,0 @@
|
|||||||
package app.revanced.manager.ui.component
|
|
||||||
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
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.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.domain.sources.RemoteSource
|
|
||||||
import app.revanced.manager.domain.sources.Source
|
|
||||||
import app.revanced.manager.ui.component.bundle.BundleInformationDialog
|
|
||||||
import app.revanced.manager.ui.viewmodel.SourcesViewModel
|
|
||||||
import app.revanced.manager.util.uiSafe
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SourceItem(
|
|
||||||
source: Source, onDelete: () -> Unit,
|
|
||||||
coroutineScope: CoroutineScope,
|
|
||||||
) {
|
|
||||||
var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val bundle by source.bundle.collectAsStateWithLifecycle()
|
|
||||||
val patchCount = bundle.patches.size
|
|
||||||
val padding = PaddingValues(16.dp, 0.dp)
|
|
||||||
|
|
||||||
val androidContext = LocalContext.current
|
|
||||||
|
|
||||||
if (viewBundleDialogPage) {
|
|
||||||
BundleInformationDialog(
|
|
||||||
onDismissRequest = { viewBundleDialogPage = false },
|
|
||||||
onDeleteRequest = {
|
|
||||||
viewBundleDialogPage = false
|
|
||||||
onDelete()
|
|
||||||
},
|
|
||||||
source = source,
|
|
||||||
patchCount = patchCount,
|
|
||||||
onRefreshButton = {
|
|
||||||
coroutineScope.launch {
|
|
||||||
uiSafe(
|
|
||||||
androidContext,
|
|
||||||
R.string.source_download_fail,
|
|
||||||
SourcesViewModel.failLogMsg
|
|
||||||
) {
|
|
||||||
if (source is RemoteSource) source.update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.height(64.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
viewBundleDialogPage = true
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = source.name,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
modifier = Modifier.padding(padding)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = pluralStringResource(R.plurals.patches_count, patchCount, patchCount),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.padding(padding)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,149 @@
|
|||||||
|
package app.revanced.manager.ui.component.bundle
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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.outlined.ArrowRight
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.revanced.manager.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BaseBundleDialog(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isDefault: Boolean,
|
||||||
|
name: String,
|
||||||
|
onNameChange: (String) -> Unit = {},
|
||||||
|
remoteUrl: String?,
|
||||||
|
onRemoteUrlChange: (String) -> Unit = {},
|
||||||
|
patchCount: Int,
|
||||||
|
version: String?,
|
||||||
|
autoUpdate: Boolean,
|
||||||
|
onAutoUpdateChange: (Boolean) -> Unit,
|
||||||
|
onPatchesClick: () -> Unit,
|
||||||
|
onBundleTypeClick: () -> Unit = {},
|
||||||
|
extraFields: @Composable ColumnScope.() -> Unit = {}
|
||||||
|
) = Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.then(modifier)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = 24.dp,
|
||||||
|
top = 16.dp,
|
||||||
|
end = 24.dp,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
value = name,
|
||||||
|
onValueChange = onNameChange,
|
||||||
|
label = {
|
||||||
|
Text(stringResource(R.string.bundle_input_name))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
remoteUrl?.takeUnless { isDefault }?.let {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
value = it,
|
||||||
|
onValueChange = onRemoteUrlChange,
|
||||||
|
label = {
|
||||||
|
Text(stringResource(R.string.bundle_input_source_url))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
extraFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier.padding(
|
||||||
|
start = 8.dp,
|
||||||
|
top = 8.dp,
|
||||||
|
end = 4.dp,
|
||||||
|
)
|
||||||
|
) Info@{
|
||||||
|
if (remoteUrl != null) {
|
||||||
|
BundleListItem(
|
||||||
|
headlineText = stringResource(R.string.automatically_update),
|
||||||
|
supportingText = stringResource(R.string.automatically_update_description),
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = autoUpdate,
|
||||||
|
onCheckedChange = onAutoUpdateChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BundleListItem(
|
||||||
|
headlineText = stringResource(R.string.bundle_type),
|
||||||
|
supportingText = stringResource(R.string.bundle_type_description)
|
||||||
|
) {
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = onBundleTypeClick,
|
||||||
|
content = {
|
||||||
|
if (remoteUrl == null) {
|
||||||
|
Text(stringResource(R.string.local))
|
||||||
|
} else {
|
||||||
|
Text(stringResource(R.string.remote))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version == null && patchCount < 1) return@Info
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.information),
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
horizontal = 16.dp,
|
||||||
|
vertical = 12.dp
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
|
||||||
|
BundleListItem(
|
||||||
|
headlineText = stringResource(R.string.patches),
|
||||||
|
supportingText = if (patchCount == 0) stringResource(R.string.no_patches)
|
||||||
|
else stringResource(R.string.patches_available, patchCount),
|
||||||
|
trailingContent = {
|
||||||
|
if (patchCount > 0) {
|
||||||
|
IconButton(onClick = onPatchesClick) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.ArrowRight,
|
||||||
|
stringResource(R.string.patches)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (version == null) return@Info
|
||||||
|
|
||||||
|
BundleListItem(
|
||||||
|
headlineText = stringResource(R.string.version),
|
||||||
|
supportingText = version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,87 +0,0 @@
|
|||||||
package app.revanced.manager.ui.component.bundle
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.ArrowRight
|
|
||||||
import androidx.compose.material3.FilledTonalButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import app.revanced.manager.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BundleInfoContent(
|
|
||||||
switchChecked: Boolean,
|
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
|
||||||
patchInfoText: String,
|
|
||||||
patchCount: Int,
|
|
||||||
onArrowClick: () -> Unit,
|
|
||||||
isLocal: Boolean,
|
|
||||||
tonalButtonOnClick: () -> Unit = {},
|
|
||||||
tonalButtonContent: @Composable RowScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
if(!isLocal) {
|
|
||||||
BundleInfoListItem(
|
|
||||||
headlineText = stringResource(R.string.automatically_update),
|
|
||||||
supportingText = stringResource(R.string.automatically_update_description),
|
|
||||||
trailingContent = {
|
|
||||||
Switch(
|
|
||||||
checked = switchChecked,
|
|
||||||
onCheckedChange = onCheckedChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BundleInfoListItem(
|
|
||||||
headlineText = stringResource(R.string.bundle_type),
|
|
||||||
supportingText = stringResource(R.string.bundle_type_description)
|
|
||||||
) {
|
|
||||||
FilledTonalButton(
|
|
||||||
onClick = tonalButtonOnClick,
|
|
||||||
content = tonalButtonContent,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.information),
|
|
||||||
modifier = Modifier.padding(
|
|
||||||
horizontal = 16.dp,
|
|
||||||
vertical = 12.dp
|
|
||||||
),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
|
|
||||||
BundleInfoListItem(
|
|
||||||
headlineText = stringResource(R.string.patches),
|
|
||||||
supportingText = patchInfoText,
|
|
||||||
trailingContent = {
|
|
||||||
if (patchCount > 0) {
|
|
||||||
IconButton(onClick = onArrowClick) {
|
|
||||||
Icon(
|
|
||||||
Icons.Outlined.ArrowRight,
|
|
||||||
stringResource(R.string.patches)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
BundleInfoListItem(
|
|
||||||
headlineText = stringResource(R.string.patches_version),
|
|
||||||
supportingText = "1.0.0",
|
|
||||||
)
|
|
||||||
|
|
||||||
BundleInfoListItem(
|
|
||||||
headlineText = stringResource(R.string.integrations_version),
|
|
||||||
supportingText = "1.0.0",
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,10 +1,6 @@
|
|||||||
package app.revanced.manager.ui.component.bundle
|
package app.revanced.manager.ui.component.bundle
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||||
@ -13,46 +9,51 @@ 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
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.sources.LocalSource
|
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
||||||
import app.revanced.manager.domain.sources.RemoteSource
|
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||||
import app.revanced.manager.domain.sources.Source
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull
|
||||||
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault
|
||||||
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.propsOrNullFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BundleInformationDialog(
|
fun BundleInformationDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDeleteRequest: () -> Unit,
|
onDeleteRequest: () -> Unit,
|
||||||
source: Source,
|
bundle: PatchBundleSource,
|
||||||
remoteName: String = "",
|
|
||||||
patchCount: Int = 0,
|
|
||||||
onRefreshButton: () -> Unit,
|
onRefreshButton: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var checked by remember { mutableStateOf(true) }
|
val composableScope = rememberCoroutineScope()
|
||||||
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
|
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
|
||||||
|
val isLocal = bundle is LocalPatchBundle
|
||||||
val isLocal = source is LocalSource
|
val patchCount by remember(bundle) {
|
||||||
|
bundle.state.map { it.patchBundleOrNull()?.patches?.size ?: 0 }
|
||||||
val patchInfoText = if (patchCount == 0) stringResource(R.string.no_patches)
|
}.collectAsStateWithLifecycle(0)
|
||||||
else stringResource(R.string.patches_available, patchCount)
|
val props by remember(bundle) {
|
||||||
|
bundle.propsOrNullFlow()
|
||||||
|
}.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
if (viewCurrentBundlePatches) {
|
if (viewCurrentBundlePatches) {
|
||||||
BundlePatchesDialog(
|
BundlePatchesDialog(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
viewCurrentBundlePatches = false
|
viewCurrentBundlePatches = false
|
||||||
},
|
},
|
||||||
source = source,
|
bundle = bundle,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,13 +76,15 @@ fun BundleInformationDialog(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = onDeleteRequest) {
|
if (!bundle.isDefault) {
|
||||||
Icon(
|
IconButton(onClick = onDeleteRequest) {
|
||||||
Icons.Outlined.DeleteOutline,
|
Icon(
|
||||||
stringResource(R.string.delete)
|
Icons.Outlined.DeleteOutline,
|
||||||
)
|
stringResource(R.string.delete)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(!isLocal) {
|
if (!isLocal) {
|
||||||
IconButton(onClick = onRefreshButton) {
|
IconButton(onClick = onRefreshButton) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Refresh,
|
Icons.Outlined.Refresh,
|
||||||
@ -93,51 +96,23 @@ fun BundleInformationDialog(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
BaseBundleDialog(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(paddingValues),
|
||||||
.fillMaxWidth()
|
isDefault = bundle.isDefault,
|
||||||
.padding(paddingValues)
|
name = bundle.name,
|
||||||
.verticalScroll(rememberScrollState())
|
remoteUrl = bundle.asRemoteOrNull?.endpoint,
|
||||||
) {
|
patchCount = patchCount,
|
||||||
Column(
|
version = props?.versionInfo?.patches,
|
||||||
modifier = Modifier.padding(
|
autoUpdate = props?.autoUpdate ?: false,
|
||||||
start = 24.dp,
|
onAutoUpdateChange = {
|
||||||
top = 16.dp,
|
composableScope.launch {
|
||||||
end = 24.dp,
|
bundle.asRemoteOrNull?.setAutoUpdate(it)
|
||||||
)
|
}
|
||||||
) {
|
},
|
||||||
BundleTextContent(
|
onPatchesClick = {
|
||||||
name = source.name,
|
viewCurrentBundlePatches = true
|
||||||
isLocal = isLocal,
|
},
|
||||||
remoteUrl = remoteName,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
Modifier.padding(
|
|
||||||
start = 8.dp,
|
|
||||||
top = 8.dp,
|
|
||||||
end = 4.dp,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
BundleInfoContent(
|
|
||||||
switchChecked = checked,
|
|
||||||
onCheckedChange = { checked = it },
|
|
||||||
patchInfoText = patchInfoText,
|
|
||||||
patchCount = patchCount,
|
|
||||||
isLocal = isLocal,
|
|
||||||
onArrowClick = {
|
|
||||||
viewCurrentBundlePatches = true
|
|
||||||
},
|
|
||||||
tonalButtonContent = {
|
|
||||||
when(source) {
|
|
||||||
is RemoteSource -> Text(stringResource(R.string.remote))
|
|
||||||
is LocalSource -> Text(stringResource(R.string.local))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,108 @@
|
|||||||
|
package app.revanced.manager.ui.component.bundle
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||||
|
import androidx.compose.material.icons.outlined.Warning
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
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.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.propsOrNullFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BundleItem(
|
||||||
|
bundle: PatchBundleSource,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onUpdate: () -> Unit
|
||||||
|
) {
|
||||||
|
var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val state by bundle.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val version by remember(bundle) {
|
||||||
|
bundle.propsOrNullFlow().map { props -> props?.versionInfo?.patches }
|
||||||
|
}.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
|
if (viewBundleDialogPage) {
|
||||||
|
BundleInformationDialog(
|
||||||
|
onDismissRequest = { viewBundleDialogPage = false },
|
||||||
|
onDeleteRequest = {
|
||||||
|
viewBundleDialogPage = false
|
||||||
|
onDelete()
|
||||||
|
},
|
||||||
|
bundle = bundle,
|
||||||
|
onRefreshButton = onUpdate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(64.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
viewBundleDialogPage = true
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = bundle.name,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
state.patchBundleOrNull()?.patches?.size?.let { patchCount ->
|
||||||
|
Text(
|
||||||
|
text = pluralStringResource(R.plurals.patches_count, patchCount, patchCount),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Row {
|
||||||
|
val icon = remember(state) {
|
||||||
|
when (state) {
|
||||||
|
is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.bundle_error
|
||||||
|
is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.bundle_missing
|
||||||
|
is PatchBundleSource.State.Loaded -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
icon?.let { (vector, description) ->
|
||||||
|
Icon(
|
||||||
|
imageVector = vector,
|
||||||
|
contentDescription = stringResource(description),
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
version?.let { txt ->
|
||||||
|
Text(
|
||||||
|
text = txt,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
@ -6,7 +6,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BundleInfoListItem(
|
fun BundleListItem(
|
||||||
headlineText: String,
|
headlineText: String,
|
||||||
supportingText: String = "",
|
supportingText: String = "",
|
||||||
trailingContent: @Composable (() -> Unit)? = null,
|
trailingContent: @Composable (() -> Unit)? = null,
|
@ -28,17 +28,17 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.sources.Source
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
import app.revanced.manager.ui.component.NotificationCard
|
import app.revanced.manager.ui.component.NotificationCard
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BundlePatchesDialog(
|
fun BundlePatchesDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
source: Source,
|
bundle: PatchBundleSource,
|
||||||
) {
|
) {
|
||||||
var informationCardVisible by remember { mutableStateOf(true) }
|
var informationCardVisible by remember { mutableStateOf(true) }
|
||||||
val bundle by source.bundle.collectAsStateWithLifecycle()
|
val state by bundle.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
@ -84,27 +84,29 @@ fun BundlePatchesDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(bundle.patches.size) { bundleIndex ->
|
state.patchBundleOrNull()?.let { bundle ->
|
||||||
val patch = bundle.patches[bundleIndex]
|
items(bundle.patches.size) { bundleIndex ->
|
||||||
ListItem(
|
val patch = bundle.patches[bundleIndex]
|
||||||
headlineContent = {
|
ListItem(
|
||||||
Text(
|
headlineContent = {
|
||||||
text = patch.name,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
},
|
|
||||||
supportingContent = {
|
|
||||||
patch.description?.let {
|
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = patch.name,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
patch.description?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
Divider()
|
||||||
Divider()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,18 +15,18 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
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.sources.Source
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceSelector(sources: List<Source>, onFinish: (Source?) -> Unit) {
|
fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSource?) -> Unit) {
|
||||||
LaunchedEffect(sources) {
|
LaunchedEffect(bundles) {
|
||||||
if (sources.size == 1) {
|
if (bundles.size == 1) {
|
||||||
onFinish(sources[0])
|
onFinish(bundles[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sources.size < 2) {
|
if (bundles.size < 2) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ fun SourceSelector(sources: List<Source>, onFinish: (Source?) -> Unit) {
|
|||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sources.forEach {
|
bundles.forEach {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
@ -1,43 +0,0 @@
|
|||||||
package app.revanced.manager.ui.component.bundle
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import app.revanced.manager.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BundleTextContent(
|
|
||||||
name: String,
|
|
||||||
onNameChange: (String) -> Unit = {},
|
|
||||||
isLocal: Boolean,
|
|
||||||
remoteUrl: String,
|
|
||||||
onRemoteUrlChange: (String) -> Unit = {},
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
value = name,
|
|
||||||
onValueChange = onNameChange,
|
|
||||||
label = {
|
|
||||||
Text(stringResource(R.string.bundle_input_name))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (!isLocal) {
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
value = remoteUrl,
|
|
||||||
onValueChange = onRemoteUrlChange,
|
|
||||||
label = {
|
|
||||||
Text(stringResource(R.string.bundle_input_source_url))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +1,21 @@
|
|||||||
package app.revanced.manager.ui.component.bundle
|
package app.revanced.manager.ui.component.bundle
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.webkit.URLUtil
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
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.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Topic
|
import androidx.compose.material.icons.filled.Topic
|
||||||
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
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@ -34,20 +31,17 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.util.APK_MIMETYPE
|
import app.revanced.manager.util.APK_MIMETYPE
|
||||||
import app.revanced.manager.util.JAR_MIMETYPE
|
import app.revanced.manager.util.JAR_MIMETYPE
|
||||||
import app.revanced.manager.util.parseUrlOrNull
|
|
||||||
import io.ktor.http.Url
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ImportBundleDialog(
|
fun ImportBundleDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onRemoteSubmit: (String, Url) -> Unit,
|
onRemoteSubmit: (String, String, Boolean) -> Unit,
|
||||||
onLocalSubmit: (String, Uri, Uri?) -> Unit,
|
onLocalSubmit: (String, Uri, Uri?) -> Unit
|
||||||
patchCount: Int = 0,
|
|
||||||
) {
|
) {
|
||||||
var name by rememberSaveable { mutableStateOf("") }
|
var name by rememberSaveable { mutableStateOf("") }
|
||||||
var remoteUrl by rememberSaveable { mutableStateOf("") }
|
var remoteUrl by rememberSaveable { mutableStateOf("") }
|
||||||
var checked by remember { mutableStateOf(true) }
|
var autoUpdate by rememberSaveable { mutableStateOf(true) }
|
||||||
var isLocal by rememberSaveable { mutableStateOf(false) }
|
var isLocal by rememberSaveable { mutableStateOf(false) }
|
||||||
var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
|
var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||||
var integrations by rememberSaveable { mutableStateOf<Uri?>(null) }
|
var integrations by rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||||
@ -58,8 +52,10 @@ fun ImportBundleDialog(
|
|||||||
val inputsAreValid by remember {
|
val inputsAreValid by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val nameSize = name.length
|
val nameSize = name.length
|
||||||
nameSize in 4..19 && if (isLocal) patchBundle != null else {
|
when {
|
||||||
remoteUrl.isNotEmpty() && remoteUrl.parseUrlOrNull() != null
|
nameSize !in 1..19 -> false
|
||||||
|
isLocal -> patchBundle != null
|
||||||
|
else -> remoteUrl.isNotEmpty() && URLUtil.isValidUrl(remoteUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,19 +64,11 @@ fun ImportBundleDialog(
|
|||||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||||
uri?.let { patchBundle = it }
|
uri?.let { patchBundle = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
val integrationsActivityLauncher =
|
val integrationsActivityLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||||
uri?.let { integrations = it }
|
uri?.let { integrations = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
val onPatchLauncherClick = {
|
|
||||||
patchActivityLauncher.launch(JAR_MIMETYPE)
|
|
||||||
}
|
|
||||||
|
|
||||||
val onIntegrationLauncherClick = {
|
|
||||||
integrationsActivityLauncher.launch(APK_MIMETYPE)
|
|
||||||
}
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
properties = DialogProperties(
|
properties = DialogProperties(
|
||||||
@ -100,115 +88,89 @@ fun ImportBundleDialog(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
Text(
|
TextButton(
|
||||||
text = stringResource(R.string.import_),
|
enabled = inputsAreValid,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
onClick = {
|
||||||
color = MaterialTheme.colorScheme.primary,
|
if (isLocal) {
|
||||||
modifier = Modifier
|
onLocalSubmit(name, patchBundle!!, integrations)
|
||||||
.padding(end = 16.dp)
|
} else {
|
||||||
.clickable {
|
onRemoteSubmit(
|
||||||
if (inputsAreValid) {
|
name,
|
||||||
if (isLocal) {
|
remoteUrl,
|
||||||
onLocalSubmit(name, patchBundle!!, integrations)
|
autoUpdate
|
||||||
} else {
|
)
|
||||||
onRemoteSubmit(name, remoteUrl.parseUrlOrNull()!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
|
modifier = Modifier.padding(end = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.import_))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
BaseBundleDialog(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(paddingValues),
|
||||||
.fillMaxWidth()
|
isDefault = false,
|
||||||
.padding(paddingValues)
|
name = name,
|
||||||
.verticalScroll(rememberScrollState())
|
onNameChange = { name = it },
|
||||||
|
remoteUrl = remoteUrl.takeUnless { isLocal },
|
||||||
|
onRemoteUrlChange = { remoteUrl = it },
|
||||||
|
patchCount = 0,
|
||||||
|
version = null,
|
||||||
|
autoUpdate = autoUpdate,
|
||||||
|
onAutoUpdateChange = { autoUpdate = it },
|
||||||
|
onPatchesClick = {},
|
||||||
|
onBundleTypeClick = { isLocal = !isLocal },
|
||||||
) {
|
) {
|
||||||
Column(
|
if (isLocal) {
|
||||||
modifier = Modifier.padding(
|
OutlinedTextField(
|
||||||
start = 24.dp,
|
modifier = Modifier
|
||||||
top = 16.dp,
|
.fillMaxWidth()
|
||||||
end = 24.dp,
|
.padding(bottom = 16.dp),
|
||||||
)
|
value = patchBundleText,
|
||||||
) {
|
onValueChange = {},
|
||||||
BundleTextContent(
|
label = {
|
||||||
name = name,
|
Text("Patches Source File")
|
||||||
onNameChange = { name = it },
|
|
||||||
isLocal = isLocal,
|
|
||||||
remoteUrl = remoteUrl,
|
|
||||||
onRemoteUrlChange = { remoteUrl = it },
|
|
||||||
)
|
|
||||||
|
|
||||||
if(isLocal) {
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
value = patchBundleText,
|
|
||||||
onValueChange = {},
|
|
||||||
label = {
|
|
||||||
Text("Patches Source File")
|
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = onPatchLauncherClick
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Topic,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
value = integrationText,
|
|
||||||
onValueChange = {},
|
|
||||||
label = {
|
|
||||||
Text("Integrations Source File")
|
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(onClick = onIntegrationLauncherClick) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Topic,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
Modifier.padding(
|
|
||||||
start = 8.dp,
|
|
||||||
top = 8.dp,
|
|
||||||
end = 4.dp,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
BundleInfoContent(
|
|
||||||
switchChecked = checked,
|
|
||||||
onCheckedChange = { checked = it },
|
|
||||||
patchInfoText = stringResource(R.string.no_patches),
|
|
||||||
patchCount = patchCount,
|
|
||||||
onArrowClick = {},
|
|
||||||
tonalButtonContent = {
|
|
||||||
if (isLocal) {
|
|
||||||
Text(stringResource(R.string.local))
|
|
||||||
} else {
|
|
||||||
Text(stringResource(R.string.remote))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
tonalButtonOnClick = { isLocal = !isLocal },
|
trailingIcon = {
|
||||||
isLocal = isLocal,
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
patchActivityLauncher.launch(JAR_MIMETYPE)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Topic,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
value = integrationText,
|
||||||
|
onValueChange = {},
|
||||||
|
label = {
|
||||||
|
Text("Integrations Source File")
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
integrationsActivityLauncher.launch(APK_MIMETYPE)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Topic,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import app.revanced.manager.ui.component.bundle.BundleItem
|
||||||
|
import app.revanced.manager.ui.viewmodel.BundlesViewModel
|
||||||
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BundlesScreen(
|
||||||
|
vm: BundlesViewModel = getViewModel(),
|
||||||
|
) {
|
||||||
|
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
sources.forEach {
|
||||||
|
BundleItem(
|
||||||
|
bundle = it,
|
||||||
|
onDelete = {
|
||||||
|
vm.delete(it)
|
||||||
|
},
|
||||||
|
onUpdate = {
|
||||||
|
vm.update(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,34 +23,66 @@ import androidx.compose.material3.TabRow
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.bundle.ImportBundleDialog
|
||||||
|
import app.revanced.manager.ui.viewmodel.DashboardViewModel
|
||||||
|
import app.revanced.manager.util.toast
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
enum class DashboardPage(
|
enum class DashboardPage(
|
||||||
val titleResId: Int,
|
val titleResId: Int,
|
||||||
val icon: ImageVector
|
val icon: ImageVector
|
||||||
) {
|
) {
|
||||||
DASHBOARD(R.string.tab_apps, Icons.Outlined.Apps),
|
DASHBOARD(R.string.tab_apps, Icons.Outlined.Apps),
|
||||||
SOURCES(R.string.tab_sources, Icons.Outlined.Source),
|
BUNDLES(R.string.tab_bundles, Icons.Outlined.Source),
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardScreen(
|
fun DashboardScreen(
|
||||||
|
vm: DashboardViewModel = getViewModel(),
|
||||||
onAppSelectorClick: () -> Unit,
|
onAppSelectorClick: () -> Unit,
|
||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
var showImportBundleDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
val pages: Array<DashboardPage> = DashboardPage.values()
|
val pages: Array<DashboardPage> = DashboardPage.values()
|
||||||
|
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
||||||
|
val androidContext = LocalContext.current
|
||||||
|
|
||||||
val pagerState = rememberPagerState()
|
val pagerState = rememberPagerState()
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
if (showImportBundleDialog) {
|
||||||
|
fun dismiss() {
|
||||||
|
showImportBundleDialog = false
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportBundleDialog(
|
||||||
|
onDismissRequest = ::dismiss,
|
||||||
|
onLocalSubmit = { name, patches, integrations ->
|
||||||
|
dismiss()
|
||||||
|
vm.createLocalSource(name, patches, integrations)
|
||||||
|
},
|
||||||
|
onRemoteSubmit = { name, url, autoUpdate ->
|
||||||
|
dismiss()
|
||||||
|
vm.createRemoteSource(name, url, autoUpdate)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
@ -66,10 +98,28 @@ fun DashboardScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(onClick = {
|
FloatingActionButton(
|
||||||
if (pagerState.currentPage == DashboardPage.DASHBOARD.ordinal)
|
onClick = {
|
||||||
onAppSelectorClick()
|
when (pagerState.currentPage) {
|
||||||
}
|
DashboardPage.DASHBOARD.ordinal -> {
|
||||||
|
if (availablePatches < 1) {
|
||||||
|
androidContext.toast(androidContext.getString(R.string.patches_unavailable))
|
||||||
|
composableScope.launch {
|
||||||
|
pagerState.animateScrollToPage(
|
||||||
|
DashboardPage.BUNDLES.ordinal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@FloatingActionButton
|
||||||
|
}
|
||||||
|
|
||||||
|
onAppSelectorClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
DashboardPage.BUNDLES.ordinal -> {
|
||||||
|
showImportBundleDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Add, stringResource(R.string.add))
|
Icon(Icons.Default.Add, stringResource(R.string.add))
|
||||||
}
|
}
|
||||||
@ -103,8 +153,8 @@ fun DashboardScreen(
|
|||||||
InstalledAppsScreen()
|
InstalledAppsScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
DashboardPage.SOURCES -> {
|
DashboardPage.BUNDLES -> {
|
||||||
SourcesScreen()
|
BundlesScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ fun PatchesSelectorScreen(
|
|||||||
val pagerState = rememberPagerState()
|
val pagerState = rememberPagerState()
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyArray())
|
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
|
||||||
if (vm.compatibleVersions.isNotEmpty())
|
if (vm.compatibleVersions.isNotEmpty())
|
||||||
UnsupportedDialog(
|
UnsupportedDialog(
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
package app.revanced.manager.ui.screen
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
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.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.ui.component.bundle.ImportBundleDialog
|
|
||||||
import app.revanced.manager.ui.component.SourceItem
|
|
||||||
import app.revanced.manager.ui.viewmodel.SourcesViewModel
|
|
||||||
import org.koin.androidx.compose.getViewModel
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SourcesScreen(
|
|
||||||
vm: SourcesViewModel = getViewModel(),
|
|
||||||
) {
|
|
||||||
var showNewSourceDialog by rememberSaveable { mutableStateOf(false) }
|
|
||||||
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
|
||||||
|
|
||||||
if (showNewSourceDialog) {
|
|
||||||
ImportBundleDialog(
|
|
||||||
onDismissRequest = { showNewSourceDialog = false },
|
|
||||||
onLocalSubmit = { name, patches, integrations ->
|
|
||||||
showNewSourceDialog = false
|
|
||||||
vm.addLocal(name, patches, integrations)
|
|
||||||
},
|
|
||||||
onRemoteSubmit = { name, url ->
|
|
||||||
showNewSourceDialog = false
|
|
||||||
vm.addRemote(name, url)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
sources.forEach {
|
|
||||||
SourceItem(
|
|
||||||
source = it,
|
|
||||||
onDelete = {
|
|
||||||
vm.delete(it)
|
|
||||||
},
|
|
||||||
coroutineScope = vm.viewModelScope
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(onClick = vm::redownloadAllSources) {
|
|
||||||
Text(stringResource(R.string.reload_sources))
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(onClick = { showNewSourceDialog = true }) {
|
|
||||||
Text("Create new source")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(onClick = vm::deleteAllSources) {
|
|
||||||
Text("Reset everything.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +1,59 @@
|
|||||||
package app.revanced.manager.ui.screen.settings
|
package app.revanced.manager.ui.screen.settings
|
||||||
|
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Http
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
import app.revanced.manager.ui.component.GroupHeader
|
||||||
|
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
||||||
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AdvancedSettingsScreen(onBackClick: () -> Unit) {
|
fun AdvancedSettingsScreen(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
vm: AdvancedSettingsViewModel = getViewModel()
|
||||||
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val memoryLimit = remember {
|
val memoryLimit = remember {
|
||||||
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
val activityManager = context.getSystemService<ActivityManager>()!!
|
||||||
context.getString(R.string.device_memory_limit_format, activityManager.memoryClass, activityManager.largeMemoryClass)
|
context.getString(
|
||||||
|
R.string.device_memory_limit_format,
|
||||||
|
activityManager.memoryClass,
|
||||||
|
activityManager.largeMemoryClass
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
@ -43,6 +68,37 @@ fun AdvancedSettingsScreen(onBackClick: () -> Unit) {
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
|
val apiUrl by vm.apiUrl.getAsState()
|
||||||
|
var showApiUrlDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showApiUrlDialog) {
|
||||||
|
APIUrlDialog(apiUrl) {
|
||||||
|
showApiUrlDialog = false
|
||||||
|
it?.let(vm::setApiUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.api_url)) },
|
||||||
|
supportingContent = { Text(apiUrl) },
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
showApiUrlDialog = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
GroupHeader(stringResource(R.string.patch_bundles_section))
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.patch_bundles_redownload)) },
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
vm.redownloadBundles()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.patch_bundles_reset)) },
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
vm.resetBundles()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.device))
|
GroupHeader(stringResource(R.string.device))
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.device_model)) },
|
headlineContent = { Text(stringResource(R.string.device_model)) },
|
||||||
@ -62,4 +118,58 @@ fun AdvancedSettingsScreen(onBackClick: () -> Unit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun APIUrlDialog(currentUrl: String, onSubmit: (String?) -> Unit) {
|
||||||
|
var url by rememberSaveable(currentUrl) { mutableStateOf(currentUrl) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { onSubmit(null) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onSubmit(url)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.api_url_dialog_save))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { onSubmit(null) }) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(Icons.Outlined.Http, null)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.api_url_dialog_title),
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.api_url_dialog_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.api_url_dialog_warning),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = url,
|
||||||
|
onValueChange = { url = it },
|
||||||
|
label = { Text(stringResource(R.string.api_url)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
@ -31,7 +31,7 @@ import app.revanced.manager.ui.viewmodel.ImportExportViewModel
|
|||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
import app.revanced.manager.ui.component.GroupHeader
|
||||||
import app.revanced.manager.ui.component.PasswordField
|
import app.revanced.manager.ui.component.PasswordField
|
||||||
import app.revanced.manager.ui.component.bundle.SourceSelector
|
import app.revanced.manager.ui.component.bundle.BundleSelector
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
@ -63,12 +63,12 @@ fun ImportExportSettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vm.selectedSource == null) {
|
if (vm.selectedBundle == null) {
|
||||||
SourceSelector(sources) {
|
BundleSelector(sources) {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
vm.clearSelectionAction()
|
vm.clearSelectionAction()
|
||||||
} else {
|
} else {
|
||||||
vm.selectSource(it)
|
vm.selectBundle(it)
|
||||||
launcher.launch(action.activityArg)
|
launcher.launch(action.activityArg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||||
|
import app.revanced.manager.util.uiSafe
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class AdvancedSettingsViewModel(
|
||||||
|
prefs: PreferencesManager,
|
||||||
|
private val app: Application,
|
||||||
|
private val patchBundleRepository: PatchBundleRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
val apiUrl = prefs.api
|
||||||
|
|
||||||
|
fun setApiUrl(value: String) = viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
if (value == apiUrl.get()) return@launch
|
||||||
|
|
||||||
|
apiUrl.update(value)
|
||||||
|
patchBundleRepository.reloadApiBundles()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun redownloadBundles() = viewModelScope.launch {
|
||||||
|
uiSafe(app, R.string.source_download_fail, RemotePatchBundle.updateFailMsg) {
|
||||||
|
patchBundleRepository.redownloadRemoteBundles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetBundles() = viewModelScope.launch {
|
||||||
|
patchBundleRepository.reset()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.util.uiSafe
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class BundlesViewModel(
|
||||||
|
private val app: Application,
|
||||||
|
private val patchBundleRepository: PatchBundleRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
val sources = patchBundleRepository.sources
|
||||||
|
|
||||||
|
fun delete(bundle: PatchBundleSource) =
|
||||||
|
viewModelScope.launch { patchBundleRepository.remove(bundle) }
|
||||||
|
|
||||||
|
fun update(bundle: PatchBundleSource) = viewModelScope.launch {
|
||||||
|
if (bundle !is RemotePatchBundle) return@launch
|
||||||
|
|
||||||
|
uiSafe(
|
||||||
|
app,
|
||||||
|
R.string.source_download_fail,
|
||||||
|
RemotePatchBundle.updateFailMsg
|
||||||
|
) {
|
||||||
|
bundle.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import io.ktor.http.Url
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class DashboardViewModel(
|
||||||
|
app: Application,
|
||||||
|
private val patchBundleRepository: PatchBundleRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
val availablePatches =
|
||||||
|
patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } }
|
||||||
|
private val contentResolver: ContentResolver = app.contentResolver
|
||||||
|
|
||||||
|
fun createLocalSource(name: String, patchBundle: Uri, integrations: Uri?) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
contentResolver.openInputStream(patchBundle)!!.use { patchesStream ->
|
||||||
|
val integrationsStream = integrations?.let { contentResolver.openInputStream(it) }
|
||||||
|
try {
|
||||||
|
patchBundleRepository.createLocal(name, patchesStream, integrationsStream)
|
||||||
|
} finally {
|
||||||
|
integrationsStream?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createRemoteSource(name: String, apiUrl: String, autoUpdate: Boolean) =
|
||||||
|
viewModelScope.launch { patchBundleRepository.createRemote(name, apiUrl, autoUpdate) }
|
||||||
|
}
|
@ -14,8 +14,8 @@ import app.revanced.manager.R
|
|||||||
import app.revanced.manager.domain.manager.KeystoreManager
|
import app.revanced.manager.domain.manager.KeystoreManager
|
||||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||||
import app.revanced.manager.domain.repository.SerializedSelection
|
import app.revanced.manager.domain.repository.SerializedSelection
|
||||||
import app.revanced.manager.domain.repository.SourceRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.domain.sources.Source
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
import app.revanced.manager.util.JSON_MIMETYPE
|
import app.revanced.manager.util.JSON_MIMETYPE
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
@ -37,11 +37,11 @@ class ImportExportViewModel(
|
|||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val keystoreManager: KeystoreManager,
|
private val keystoreManager: KeystoreManager,
|
||||||
private val selectionRepository: PatchSelectionRepository,
|
private val selectionRepository: PatchSelectionRepository,
|
||||||
sourceRepository: SourceRepository
|
patchBundleRepository: PatchBundleRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val contentResolver = app.contentResolver
|
private val contentResolver = app.contentResolver
|
||||||
val sources = sourceRepository.sources
|
val sources = patchBundleRepository.sources
|
||||||
var selectedSource by mutableStateOf<Source?>(null)
|
var selectedBundle by mutableStateOf<PatchBundleSource?>(null)
|
||||||
private set
|
private set
|
||||||
var selectionAction by mutableStateOf<SelectionAction?>(null)
|
var selectionAction by mutableStateOf<SelectionAction?>(null)
|
||||||
private set
|
private set
|
||||||
@ -107,20 +107,20 @@ class ImportExportViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun executeSelectionAction(target: Uri) = viewModelScope.launch {
|
fun executeSelectionAction(target: Uri) = viewModelScope.launch {
|
||||||
val source = selectedSource!!
|
val source = selectedBundle!!
|
||||||
val action = selectionAction!!
|
val action = selectionAction!!
|
||||||
clearSelectionAction()
|
clearSelectionAction()
|
||||||
|
|
||||||
action.execute(source, target)
|
action.execute(source.uid, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectSource(source: Source) {
|
fun selectBundle(bundle: PatchBundleSource) {
|
||||||
selectedSource = source
|
selectedBundle = bundle
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearSelectionAction() {
|
fun clearSelectionAction() {
|
||||||
selectionAction = null
|
selectionAction = null
|
||||||
selectedSource = null
|
selectedBundle = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importSelection() = clearSelectionAction().also {
|
fun importSelection() = clearSelectionAction().also {
|
||||||
@ -132,7 +132,7 @@ class ImportExportViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed interface SelectionAction {
|
sealed interface SelectionAction {
|
||||||
suspend fun execute(source: Source, location: Uri)
|
suspend fun execute(bundleUid: Int, location: Uri)
|
||||||
val activityContract: ActivityResultContract<String, Uri?>
|
val activityContract: ActivityResultContract<String, Uri?>
|
||||||
val activityArg: String
|
val activityArg: String
|
||||||
}
|
}
|
||||||
@ -140,7 +140,7 @@ class ImportExportViewModel(
|
|||||||
private inner class Import : SelectionAction {
|
private inner class Import : SelectionAction {
|
||||||
override val activityContract = ActivityResultContracts.GetContent()
|
override val activityContract = ActivityResultContracts.GetContent()
|
||||||
override val activityArg = JSON_MIMETYPE
|
override val activityArg = JSON_MIMETYPE
|
||||||
override suspend fun execute(source: Source, location: Uri) = uiSafe(
|
override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe(
|
||||||
app,
|
app,
|
||||||
R.string.restore_patches_selection_fail,
|
R.string.restore_patches_selection_fail,
|
||||||
"Failed to restore patches selection"
|
"Failed to restore patches selection"
|
||||||
@ -151,19 +151,19 @@ class ImportExportViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectionRepository.import(source, selection)
|
selectionRepository.import(bundleUid, selection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class Export : SelectionAction {
|
private inner class Export : SelectionAction {
|
||||||
override val activityContract = ActivityResultContracts.CreateDocument(JSON_MIMETYPE)
|
override val activityContract = ActivityResultContracts.CreateDocument(JSON_MIMETYPE)
|
||||||
override val activityArg = "selection.json"
|
override val activityArg = "selection.json"
|
||||||
override suspend fun execute(source: Source, location: Uri) = uiSafe(
|
override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe(
|
||||||
app,
|
app,
|
||||||
R.string.backup_patches_selection_fail,
|
R.string.backup_patches_selection_fail,
|
||||||
"Failed to backup patches selection"
|
"Failed to backup patches selection"
|
||||||
) {
|
) {
|
||||||
val selection = selectionRepository.export(source)
|
val selection = selectionRepository.export(bundleUid)
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
contentResolver.openOutputStream(location, "wt")!!.use {
|
contentResolver.openOutputStream(location, "wt")!!.use {
|
||||||
|
@ -2,16 +2,31 @@ package app.revanced.manager.ui.viewmodel
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.domain.repository.SourceRepository
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
sourceRepository: SourceRepository
|
private val patchBundleRepository: PatchBundleRepository,
|
||||||
|
val prefs: PreferencesManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
init {
|
|
||||||
with(viewModelScope) {
|
fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch {
|
||||||
launch {
|
prefs.showAutoUpdatesDialog.update(false)
|
||||||
sourceRepository.loadSources()
|
|
||||||
|
prefs.managerAutoUpdates.update(manager)
|
||||||
|
if (patches) {
|
||||||
|
with(patchBundleRepository) {
|
||||||
|
sources
|
||||||
|
.first()
|
||||||
|
.find { it.uid == 0 }
|
||||||
|
?.asRemoteOrNull
|
||||||
|
?.setAutoUpdate(true)
|
||||||
|
|
||||||
|
updateCheck()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
|||||||
import androidx.lifecycle.viewmodel.compose.saveable
|
import androidx.lifecycle.viewmodel.compose.saveable
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||||
import app.revanced.manager.domain.repository.SourceRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
@ -43,21 +43,23 @@ class PatchesSelectorViewModel(
|
|||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
|
|
||||||
val allowExperimental = get<PreferencesManager>().allowExperimental
|
val allowExperimental = get<PreferencesManager>().allowExperimental
|
||||||
val bundlesFlow = get<SourceRepository>().sources.flatMapLatestAndCombine(
|
val bundlesFlow = get<PatchBundleRepository>().sources.flatMapLatestAndCombine(
|
||||||
combiner = { it }
|
combiner = { it.filterNotNull() }
|
||||||
) { source ->
|
) { source ->
|
||||||
// Regenerate bundle information whenever this source updates.
|
// Regenerate bundle information whenever this source updates.
|
||||||
source.bundle.map { bundle ->
|
source.state.map { state ->
|
||||||
|
val bundle = state.patchBundleOrNull() ?: return@map null
|
||||||
|
|
||||||
val supported = mutableListOf<PatchInfo>()
|
val supported = mutableListOf<PatchInfo>()
|
||||||
val unsupported = mutableListOf<PatchInfo>()
|
val unsupported = mutableListOf<PatchInfo>()
|
||||||
val universal = mutableListOf<PatchInfo>()
|
val universal = mutableListOf<PatchInfo>()
|
||||||
|
|
||||||
bundle.patches.filter { it.compatibleWith(selectedApp.packageName) }.forEach {
|
bundle.patches.filter { it.compatibleWith(selectedApp.packageName) }.forEach {
|
||||||
val targetList =
|
val targetList = when {
|
||||||
if (it.compatiblePackages == null) universal else if (it.supportsVersion(selectedApp.version))
|
it.compatiblePackages == null -> universal
|
||||||
supported
|
it.supportsVersion(selectedApp.version) -> supported
|
||||||
else
|
else -> unsupported
|
||||||
unsupported
|
}
|
||||||
|
|
||||||
targetList.add(it)
|
targetList.add(it)
|
||||||
}
|
}
|
||||||
@ -66,30 +68,36 @@ class PatchesSelectorViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val selectedPatches: SnapshotStatePatchesSelection by savedStateHandle.saveable(saver = patchesSelectionSaver, init = {
|
private val selectedPatches: SnapshotStatePatchesSelection by savedStateHandle.saveable(
|
||||||
val map: SnapshotStatePatchesSelection = mutableStateMapOf()
|
saver = patchesSelectionSaver,
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
init = {
|
||||||
val bundles = bundlesFlow.first()
|
val map: SnapshotStatePatchesSelection = mutableStateMapOf()
|
||||||
val filteredSelection =
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
selectionRepository.getSelection(selectedApp.packageName).mapValues { (uid, patches) ->
|
val bundles = bundlesFlow.first()
|
||||||
// Filter out patches that don't exist.
|
val filteredSelection =
|
||||||
val filteredPatches = bundles.singleOrNull { it.uid == uid }
|
selectionRepository.getSelection(selectedApp.packageName)
|
||||||
?.let { bundle ->
|
.mapValues { (uid, patches) ->
|
||||||
val allPatches = bundle.all.map { it.name }
|
// Filter out patches that don't exist.
|
||||||
patches.filter { allPatches.contains(it) }
|
val filteredPatches = bundles.singleOrNull { it.uid == uid }
|
||||||
|
?.let { bundle ->
|
||||||
|
val allPatches = bundle.all.map { it.name }
|
||||||
|
patches.filter { allPatches.contains(it) }
|
||||||
|
}
|
||||||
|
?: patches
|
||||||
|
|
||||||
|
filteredPatches.toMutableStateSet()
|
||||||
}
|
}
|
||||||
?: patches
|
|
||||||
|
|
||||||
filteredPatches.toMutableStateSet()
|
withContext(Dispatchers.Main) {
|
||||||
|
map.putAll(filteredSelection)
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
map.putAll(filteredSelection)
|
|
||||||
}
|
}
|
||||||
}
|
return@saveable map
|
||||||
return@saveable map
|
})
|
||||||
})
|
private val patchOptions: SnapshotStateOptions by savedStateHandle.saveable(
|
||||||
private val patchOptions: SnapshotStateOptions by savedStateHandle.saveable(saver = optionsSaver, init = ::mutableStateMapOf)
|
saver = optionsSaver,
|
||||||
|
init = ::mutableStateMapOf
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the patch options dialog for this patch.
|
* Show the patch options dialog for this patch.
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.domain.sources.Source
|
|
||||||
import app.revanced.manager.domain.repository.SourceRepository
|
|
||||||
import app.revanced.manager.util.uiSafe
|
|
||||||
import io.ktor.http.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class SourcesViewModel(
|
|
||||||
private val app: Application,
|
|
||||||
private val sourceRepository: SourceRepository
|
|
||||||
) : ViewModel() {
|
|
||||||
val sources = sourceRepository.sources
|
|
||||||
private val contentResolver: ContentResolver = app.contentResolver
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val failLogMsg = "Failed to update patch bundle(s)"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun redownloadAllSources() = viewModelScope.launch {
|
|
||||||
uiSafe(app, R.string.source_download_fail, failLogMsg) {
|
|
||||||
sourceRepository.redownloadRemoteSources()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addLocal(name: String, patchBundle: Uri, integrations: Uri?) = viewModelScope.launch {
|
|
||||||
contentResolver.openInputStream(patchBundle)!!.use { patchesStream ->
|
|
||||||
val integrationsStream = integrations?.let { contentResolver.openInputStream(it) }
|
|
||||||
try {
|
|
||||||
sourceRepository.createLocalSource(name, patchesStream, integrationsStream)
|
|
||||||
} finally {
|
|
||||||
integrationsStream?.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addRemote(name: String, apiUrl: Url) =
|
|
||||||
viewModelScope.launch { sourceRepository.createRemoteSource(name, apiUrl) }
|
|
||||||
|
|
||||||
fun delete(source: Source) = viewModelScope.launch { sourceRepository.remove(source) }
|
|
||||||
|
|
||||||
fun deleteAllSources() = viewModelScope.launch {
|
|
||||||
sourceRepository.resetConfig()
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,7 +8,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||||
import app.revanced.manager.domain.repository.SourceRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.network.downloader.APKMirror
|
import app.revanced.manager.network.downloader.APKMirror
|
||||||
import app.revanced.manager.network.downloader.AppDownloader
|
import app.revanced.manager.network.downloader.AppDownloader
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
@ -28,7 +28,7 @@ class VersionSelectorViewModel(
|
|||||||
val packageName: String
|
val packageName: String
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
||||||
private val sourceRepository: SourceRepository by inject()
|
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||||
private val pm: PM by inject()
|
private val pm: PM by inject()
|
||||||
private val appDownloader: AppDownloader = APKMirror()
|
private val appDownloader: AppDownloader = APKMirror()
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ class VersionSelectorViewModel(
|
|||||||
|
|
||||||
val downloadableVersions = mutableStateSetOf<SelectedApp.Download>()
|
val downloadableVersions = mutableStateSetOf<SelectedApp.Download>()
|
||||||
|
|
||||||
val supportedVersions = sourceRepository.bundles.map { bundles ->
|
val supportedVersions = patchBundleRepository.bundles.map { bundles ->
|
||||||
var patchesWithoutVersions = 0
|
var patchesWithoutVersions = 0
|
||||||
|
|
||||||
bundles.flatMap { (_, bundle) ->
|
bundles.flatMap { (_, bundle) ->
|
||||||
|
@ -8,7 +8,6 @@ const val ghPatcher = "$team/revanced-patcher"
|
|||||||
const val ghManager = "$team/revanced-manager"
|
const val ghManager = "$team/revanced-manager"
|
||||||
const val ghIntegrations = "$team/revanced-integrations"
|
const val ghIntegrations = "$team/revanced-integrations"
|
||||||
const val tag = "ReVanced Manager"
|
const val tag = "ReVanced Manager"
|
||||||
const val apiURL = "https://releases.revanced.app"
|
|
||||||
|
|
||||||
const val JAR_MIMETYPE = "application/java-archive"
|
const val JAR_MIMETYPE = "application/java-archive"
|
||||||
const val APK_MIMETYPE = "application/vnd.android.package-archive"
|
const val APK_MIMETYPE = "application/vnd.android.package-archive"
|
||||||
|
@ -13,7 +13,7 @@ import android.content.pm.PackageManager.NameNotFoundException
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import app.revanced.manager.domain.repository.SourceRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.service.InstallService
|
import app.revanced.manager.service.InstallService
|
||||||
import app.revanced.manager.service.UninstallService
|
import app.revanced.manager.service.UninstallService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -40,11 +40,11 @@ data class AppInfo(
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
class PM(
|
class PM(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
sourceRepository: SourceRepository
|
patchBundleRepository: PatchBundleRepository
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
val appList = sourceRepository.bundles.map { bundles ->
|
val appList = patchBundleRepository.bundles.map { bundles ->
|
||||||
val compatibleApps = scope.async {
|
val compatibleApps = scope.async {
|
||||||
val compatiblePackages = bundles.values
|
val compatiblePackages = bundles.values
|
||||||
.flatMap { it.patches }
|
.flatMap { it.patches }
|
||||||
|
@ -11,7 +11,6 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import io.ktor.http.Url
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -33,12 +32,6 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
|||||||
Toast.makeText(this, string, duration).show()
|
Toast.makeText(this, string, duration).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.parseUrlOrNull() = try {
|
|
||||||
Url(this)
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely perform an operation that may fail to avoid crashing the app.
|
* Safely perform an operation that may fail to avoid crashing the app.
|
||||||
* If [block] fails, the error will be logged and a toast will be shown to the user to inform them that the action failed.
|
* If [block] fails, the error will be logged and a toast will be shown to the user to inform them that the action failed.
|
||||||
|
@ -12,11 +12,20 @@
|
|||||||
<string name="select_patches">Select patches</string>
|
<string name="select_patches">Select patches</string>
|
||||||
|
|
||||||
<string name="import_">Import</string>
|
<string name="import_">Import</string>
|
||||||
<string name="import_bundle">Import Bundle</string>
|
<string name="import_bundle">Import patch bundle</string>
|
||||||
<string name="bundle_information">Bundle information</string>
|
<string name="bundle_information">Bundle information</string>
|
||||||
<string name="bundle_patches">Bundle patches</string>
|
<string name="bundle_patches">Bundle patches</string>
|
||||||
|
|
||||||
|
<string name="bundle_missing">Missing</string>
|
||||||
|
<string name="bundle_error">Error</string>
|
||||||
|
|
||||||
<string name="select_version">Select version</string>
|
<string name="select_version">Select version</string>
|
||||||
|
|
||||||
|
<string name="auto_updates_dialog_title">Select updates to receive</string>
|
||||||
|
<string name="auto_updates_dialog_description">Periodically connect to update providers to check for updates.</string>
|
||||||
|
<string name="auto_updates_dialog_manager">Manager updates</string>
|
||||||
|
<string name="auto_updates_dialog_patches">Patches</string>
|
||||||
|
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
||||||
|
|
||||||
<string name="general">General</string>
|
<string name="general">General</string>
|
||||||
<string name="general_description">General settings</string>
|
<string name="general_description">General settings</string>
|
||||||
@ -90,22 +99,30 @@
|
|||||||
<string name="dark">Dark</string>
|
<string name="dark">Dark</string>
|
||||||
<string name="appearance">Appearance</string>
|
<string name="appearance">Appearance</string>
|
||||||
<string name="downloaded_apps">Downloaded apps</string>
|
<string name="downloaded_apps">Downloaded apps</string>
|
||||||
|
<string name="api_url">API URL</string>
|
||||||
|
<string name="api_url_dialog_title">Set custom API URL</string>
|
||||||
|
<string name="api_url_dialog_description">You may have issues with features when using a custom API URL.</string>
|
||||||
|
<string name="api_url_dialog_warning">Only use API\'s you trust!</string>
|
||||||
|
<string name="api_url_dialog_save">Set</string>
|
||||||
<string name="device">Device</string>
|
<string name="device">Device</string>
|
||||||
<string name="device_android_version">Android version</string>
|
<string name="device_android_version">Android version</string>
|
||||||
<string name="device_model">Model</string>
|
<string name="device_model">Model</string>
|
||||||
<string name="device_architectures">CPU Architectures</string>
|
<string name="device_architectures">CPU Architectures</string>
|
||||||
<string name="device_memory_limit">Memory limits</string>
|
<string name="device_memory_limit">Memory limits</string>
|
||||||
<string name="device_memory_limit_format">Normal: %1$d MB, Large: %2$d MB</string>
|
<string name="device_memory_limit_format">Normal: %1$d MB, Large: %2$d MB</string>
|
||||||
|
<string name="patch_bundles_section">Patch bundles</string>
|
||||||
|
<string name="patch_bundles_redownload">Redownload all patch bundles</string>
|
||||||
|
<string name="patch_bundles_reset">Reset patch bundles</string>
|
||||||
<string name="patching">Patching</string>
|
<string name="patching">Patching</string>
|
||||||
<string name="signing">Signing</string>
|
<string name="signing">Signing</string>
|
||||||
<string name="storage">Storage</string>
|
<string name="storage">Storage</string>
|
||||||
|
<string name="patches_unavailable">No patches are available. Check your bundles</string>
|
||||||
<string name="tab_apps">Apps</string>
|
<string name="tab_apps">Apps</string>
|
||||||
<string name="tab_sources">Sources</string>
|
<string name="tab_bundles">Patch bundles</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
<string name="refresh">Refresh</string>
|
<string name="refresh">Refresh</string>
|
||||||
<string name="remote">Remote</string>
|
<string name="remote">Remote</string>
|
||||||
<string name="local">Local</string>
|
<string name="local">Local</string>
|
||||||
<string name="reload_sources">Reload all sources</string>
|
|
||||||
<string name="continue_anyways">Continue anyways</string>
|
<string name="continue_anyways">Continue anyways</string>
|
||||||
<string name="download_another_version">Download another version</string>
|
<string name="download_another_version">Download another version</string>
|
||||||
<string name="download_app">Download app</string>
|
<string name="download_app">Download app</string>
|
||||||
@ -181,8 +198,6 @@
|
|||||||
<string name="automatically_update_description">Automatically update this bundle when ReVanced starts</string>
|
<string name="automatically_update_description">Automatically update this bundle when ReVanced starts</string>
|
||||||
<string name="bundle_type">Bundle type</string>
|
<string name="bundle_type">Bundle type</string>
|
||||||
<string name="bundle_type_description">Choose the type of bundle you want</string>
|
<string name="bundle_type_description">Choose the type of bundle you want</string>
|
||||||
<string name="patches_version">Patches version</string>
|
|
||||||
<string name="integrations_version">Integrations version</string>
|
|
||||||
<string name="about_revanced_manager">About ReVanced Manager</string>
|
<string name="about_revanced_manager">About ReVanced Manager</string>
|
||||||
<string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string>
|
<string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string>
|
||||||
<string name="update_notification">A minor update for ReVanced Manager is available. Click here to update and get the latest features and fixes!</string>
|
<string name="update_notification">A minor update for ReVanced Manager is available. Click here to update and get the latest features and fixes!</string>
|
||||||
@ -199,6 +214,7 @@
|
|||||||
<string name="downloading_manager_update">Downloading update…</string>
|
<string name="downloading_manager_update">Downloading update…</string>
|
||||||
<string name="download_manager_failed">Failed to download update: %s</string>
|
<string name="download_manager_failed">Failed to download update: %s</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
|
<string name="save">Save</string>
|
||||||
<string name="update">Update</string>
|
<string name="update">Update</string>
|
||||||
<string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will close when updating.</string>
|
<string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will close when updating.</string>
|
||||||
</resources>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user