mirror of
https://github.com/revanced/revanced-manager
synced 2024-05-14 13:56:57 +02:00
feat: finish implementing the sources system (#70)
This commit is contained in:
parent
299aaa2b68
commit
379ce917a9
@ -2,11 +2,11 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "f7e0fef1b937143a8b128e3dbab7c041",
|
||||
"identityHash": "7142188e25ce489eb233aed8fb76e4cc",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "sources",
|
||||
"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`))",
|
||||
"tableName": "patch_bundles",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, `version` TEXT, `integrations_version` TEXT, PRIMARY KEY(`uid`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uid",
|
||||
@ -21,22 +21,28 @@
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "location",
|
||||
"columnName": "location",
|
||||
"fieldPath": "source",
|
||||
"columnName": "source",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoUpdate",
|
||||
"columnName": "auto_update",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "versionInfo.patches",
|
||||
"columnName": "version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "versionInfo.integrations",
|
||||
"columnName": "integrations_version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@ -47,20 +53,20 @@
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_sources_name",
|
||||
"name": "index_patch_bundles_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"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": []
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"fieldPath": "uid",
|
||||
@ -69,8 +75,8 @@
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "source",
|
||||
"columnName": "source",
|
||||
"fieldPath": "patchBundle",
|
||||
"columnName": "patch_bundle",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
@ -89,23 +95,23 @@
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_patch_selections_source_package_name",
|
||||
"name": "index_patch_selections_patch_bundle_package_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"source",
|
||||
"patch_bundle",
|
||||
"package_name"
|
||||
],
|
||||
"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": [
|
||||
{
|
||||
"table": "sources",
|
||||
"table": "patch_bundles",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"source"
|
||||
"patch_bundle"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uid"
|
||||
@ -189,7 +195,7 @@
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"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"
|
||||
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.REQUEST_INSTALL_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.runtime.getValue
|
||||
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.screen.VersionSelectorScreen
|
||||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||
@ -28,22 +28,19 @@ import dev.olshevski.navigation.reimagined.popUpTo
|
||||
import dev.olshevski.navigation.reimagined.rememberNavController
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import kotlin.math.roundToInt
|
||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val prefs: PreferencesManager = get()
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
installSplashScreen()
|
||||
val vm: MainViewModel = getActivityViewModel()
|
||||
|
||||
getActivityViewModel<MainViewModel>()
|
||||
installSplashScreen()
|
||||
|
||||
val scale = this.resources.displayMetrics.density
|
||||
val pixels = (36 * scale).roundToInt()
|
||||
@ -57,8 +54,8 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
|
||||
setContent {
|
||||
val theme by prefs.theme.getAsState()
|
||||
val dynamicColor by prefs.dynamicColor.getAsState()
|
||||
val theme by vm.prefs.theme.getAsState()
|
||||
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||
|
||||
ReVancedManagerTheme(
|
||||
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
||||
@ -69,6 +66,11 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
NavBackHandler(navController)
|
||||
|
||||
val showAutoUpdatesDialog by vm.prefs.showAutoUpdatesDialog.getAsState()
|
||||
if (showAutoUpdatesDialog) {
|
||||
AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
|
||||
}
|
||||
|
||||
AnimatedNavHost(
|
||||
controller = navController
|
||||
) { destination ->
|
||||
|
@ -3,6 +3,8 @@ package app.revanced.manager
|
||||
import android.app.Application
|
||||
import app.revanced.manager.di.*
|
||||
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.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
@ -14,6 +16,7 @@ import org.koin.core.context.startKoin
|
||||
class ManagerApplication : Application() {
|
||||
private val scope = MainScope()
|
||||
private val prefs: PreferencesManager by inject()
|
||||
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
@ -36,5 +39,11 @@ class ManagerApplication : Application() {
|
||||
scope.launch {
|
||||
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.SelectedPatch
|
||||
import app.revanced.manager.data.room.selection.SelectionDao
|
||||
import app.revanced.manager.data.room.sources.SourceDao
|
||||
import app.revanced.manager.data.room.sources.SourceEntity
|
||||
import app.revanced.manager.data.room.bundles.PatchBundleDao
|
||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||
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)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun sourceDao(): SourceDao
|
||||
abstract fun patchBundleDao(): PatchBundleDao
|
||||
abstract fun selectionDao(): SelectionDao
|
||||
abstract fun appDao(): AppDao
|
||||
|
||||
|
@ -1,19 +1,16 @@
|
||||
package app.revanced.manager.data.room
|
||||
|
||||
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 java.io.File
|
||||
|
||||
class Converters {
|
||||
@TypeConverter
|
||||
fun locationFromString(value: String) = when(value) {
|
||||
SourceLocation.Local.SENTINEL -> SourceLocation.Local
|
||||
else -> SourceLocation.Remote(Url(value))
|
||||
}
|
||||
fun sourceFromString(value: String) = Source.from(value)
|
||||
|
||||
@TypeConverter
|
||||
fun locationToString(location: SourceLocation) = location.toString()
|
||||
fun sourceToString(value: Source) = value.toString()
|
||||
|
||||
@TypeConverter
|
||||
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.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import app.revanced.manager.data.room.sources.SourceEntity
|
||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||
|
||||
@Entity(
|
||||
tableName = "patch_selections",
|
||||
foreignKeys = [ForeignKey(
|
||||
SourceEntity::class,
|
||||
PatchBundleEntity::class,
|
||||
parentColumns = ["uid"],
|
||||
childColumns = ["source"],
|
||||
childColumns = ["patch_bundle"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
indices = [Index(value = ["source", "package_name"], unique = true)]
|
||||
indices = [Index(value = ["patch_bundle", "package_name"], unique = true)]
|
||||
)
|
||||
data class PatchSelection(
|
||||
@PrimaryKey val uid: Int,
|
||||
@ColumnInfo(name = "source") val source: Int,
|
||||
@ColumnInfo(name = "patch_bundle") val patchBundle: Int,
|
||||
@ColumnInfo(name = "package_name") val packageName: String
|
||||
)
|
@ -9,9 +9,9 @@ import androidx.room.Transaction
|
||||
@Dao
|
||||
abstract class SelectionDao {
|
||||
@Transaction
|
||||
@MapInfo(keyColumn = "source", valueColumn = "patch_name")
|
||||
@MapInfo(keyColumn = "patch_bundle", valueColumn = "patch_name")
|
||||
@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" +
|
||||
" WHERE package_name = :packageName"
|
||||
)
|
||||
@ -22,18 +22,18 @@ abstract class SelectionDao {
|
||||
@Query(
|
||||
"SELECT package_name, patch_name FROM patch_selections" +
|
||||
" 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")
|
||||
abstract suspend fun getSelectionId(sourceUid: Int, packageName: String): Int?
|
||||
@Query("SELECT uid FROM patch_selections WHERE patch_bundle = :bundleUid AND package_name = :packageName")
|
||||
abstract suspend fun getSelectionId(bundleUid: Int, packageName: String): Int?
|
||||
|
||||
@Insert
|
||||
abstract suspend fun createSelection(selection: PatchSelection)
|
||||
|
||||
@Query("DELETE FROM patch_selections WHERE source = :uid")
|
||||
abstract suspend fun clearForSource(uid: Int)
|
||||
@Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
|
||||
abstract suspend fun clearForPatchBundle(uid: Int)
|
||||
|
||||
@Query("DELETE FROM patch_selections")
|
||||
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
|
||||
|
||||
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.worker.WorkerRepository
|
||||
import app.revanced.manager.network.api.ManagerAPI
|
||||
@ -12,9 +13,10 @@ val repositoryModule = module {
|
||||
singleOf(::GithubRepository)
|
||||
singleOf(::ManagerAPI)
|
||||
singleOf(::FileSystem)
|
||||
singleOf(::SourcePersistenceRepository)
|
||||
singleOf(::NetworkInfo)
|
||||
singleOf(::PatchBundlePersistenceRepository)
|
||||
singleOf(::PatchSelectionRepository)
|
||||
singleOf(::SourceRepository)
|
||||
singleOf(::PatchBundleRepository)
|
||||
singleOf(::WorkerRepository)
|
||||
singleOf(::DownloadedAppRepository)
|
||||
}
|
@ -6,11 +6,13 @@ import org.koin.dsl.module
|
||||
|
||||
val viewModelModule = module {
|
||||
viewModelOf(::MainViewModel)
|
||||
viewModelOf(::DashboardViewModel)
|
||||
viewModelOf(::PatchesSelectorViewModel)
|
||||
viewModelOf(::SettingsViewModel)
|
||||
viewModelOf(::AdvancedSettingsViewModel)
|
||||
viewModelOf(::AppSelectorViewModel)
|
||||
viewModelOf(::VersionSelectorViewModel)
|
||||
viewModelOf(::SourcesViewModel)
|
||||
viewModelOf(::BundlesViewModel)
|
||||
viewModelOf(::InstallerViewModel)
|
||||
viewModelOf(::UpdateProgressViewModel)
|
||||
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.withContext
|
||||
@ -7,17 +7,17 @@ import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
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) {
|
||||
withContext(Dispatchers.IO) {
|
||||
patches?.let {
|
||||
Files.copy(it, patchesJar.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
Files.copy(it, patchesFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
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 theme = enumPreference("theme", Theme.SYSTEM)
|
||||
|
||||
val api = stringPreference("api_url", "https://releases.revanced.app")
|
||||
|
||||
val allowExperimental = booleanPreference("allow_experimental", false)
|
||||
|
||||
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
||||
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
||||
|
||||
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.Companion.generateUid
|
||||
import app.revanced.manager.data.room.selection.PatchSelection
|
||||
import app.revanced.manager.domain.sources.Source
|
||||
|
||||
class PatchSelectionRepository(db: AppDatabase) {
|
||||
private val dao = db.selectionDao()
|
||||
|
||||
private suspend fun getOrCreateSelection(sourceUid: Int, packageName: String) =
|
||||
dao.getSelectionId(sourceUid, packageName) ?: PatchSelection(
|
||||
private suspend fun getOrCreateSelection(bundleUid: Int, packageName: String) =
|
||||
dao.getSelectionId(bundleUid, packageName) ?: PatchSelection(
|
||||
uid = generateUid(),
|
||||
source = sourceUid,
|
||||
patchBundle = bundleUid,
|
||||
packageName = packageName
|
||||
).also { dao.createSelection(it) }.uid
|
||||
|
||||
@ -28,12 +27,12 @@ class PatchSelectionRepository(db: AppDatabase) {
|
||||
|
||||
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) {
|
||||
dao.clearForSource(source.uid)
|
||||
suspend fun import(bundleUid: Int, selection: SerializedSelection) {
|
||||
dao.clearForPatchBundle(bundleUid)
|
||||
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
|
||||
|
||||
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.utils.getOrThrow
|
||||
|
||||
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
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import app.revanced.manager.domain.repository.Assets
|
||||
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 io.ktor.client.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.util.cio.*
|
||||
import io.ktor.utils.io.*
|
||||
import io.ktor.client.plugins.onDownload
|
||||
import io.ktor.client.request.url
|
||||
import java.io.File
|
||||
|
||||
// TODO: merge ReVancedRepository into this class
|
||||
class ManagerAPI(
|
||||
private val client: HttpClient,
|
||||
private val http: HttpService,
|
||||
private val revancedRepository: ReVancedRepository
|
||||
) {
|
||||
var downloadProgress: Float? by mutableStateOf(null)
|
||||
var downloadedSize: Long? by mutableStateOf(null)
|
||||
var totalSize: Long? by mutableStateOf(null)
|
||||
|
||||
private suspend fun downloadAsset(downloadUrl: String, saveLocation: File) {
|
||||
client.get(downloadUrl) {
|
||||
private suspend fun downloadAsset(asset: Asset, saveLocation: File) {
|
||||
http.download(saveLocation) {
|
||||
url(asset.downloadUrl)
|
||||
onDownload { bytesSentTotal, contentLength ->
|
||||
downloadProgress = (bytesSentTotal.toFloat() / contentLength.toFloat())
|
||||
downloadedSize = bytesSentTotal
|
||||
totalSize = contentLength
|
||||
}
|
||||
}.bodyAsChannel().copyAndClose(saveLocation.writeChannel())
|
||||
}
|
||||
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) {
|
||||
val managerAsset = revancedRepository.findAsset(ghManager, ".apk")
|
||||
downloadAsset(managerAsset.downloadUrl, location)
|
||||
val managerAsset = revancedRepository.getAssets().find(ghManager, ".apk")
|
||||
downloadAsset(managerAsset, location)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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.utils.APIResponse
|
||||
import app.revanced.manager.network.utils.getOrThrow
|
||||
import app.revanced.manager.util.apiURL
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -14,33 +13,20 @@ import kotlinx.coroutines.withContext
|
||||
class ReVancedService(
|
||||
private val client: HttpService,
|
||||
) {
|
||||
suspend fun getAssets(): APIResponse<ReVancedReleases> {
|
||||
suspend fun getAssets(api: String): APIResponse<ReVancedReleases> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
client.request {
|
||||
url("$apiUrl/tools")
|
||||
url("$api/tools")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContributors(): APIResponse<ReVancedRepositories> {
|
||||
suspend fun getContributors(api: String): APIResponse<ReVancedRepositories> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
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.domain.manager.PreferencesManager
|
||||
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.WorkerRepository
|
||||
import app.revanced.manager.patcher.Session
|
||||
@ -43,7 +43,7 @@ class PatcherWorker(
|
||||
parameters: WorkerParameters
|
||||
) : 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 prefs: PreferencesManager by inject()
|
||||
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
||||
@ -124,7 +124,7 @@ class PatcherWorker(
|
||||
val frameworkPath =
|
||||
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 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
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.filled.ArrowBack
|
||||
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.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
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.domain.bundles.LocalPatchBundle
|
||||
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||
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)
|
||||
@Composable
|
||||
fun BundleInformationDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDeleteRequest: () -> Unit,
|
||||
source: Source,
|
||||
remoteName: String = "",
|
||||
patchCount: Int = 0,
|
||||
bundle: PatchBundleSource,
|
||||
onRefreshButton: () -> Unit,
|
||||
) {
|
||||
var checked by remember { mutableStateOf(true) }
|
||||
val composableScope = rememberCoroutineScope()
|
||||
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
|
||||
|
||||
val isLocal = source is LocalSource
|
||||
|
||||
val patchInfoText = if (patchCount == 0) stringResource(R.string.no_patches)
|
||||
else stringResource(R.string.patches_available, patchCount)
|
||||
val isLocal = bundle is LocalPatchBundle
|
||||
val patchCount by remember(bundle) {
|
||||
bundle.state.map { it.patchBundleOrNull()?.patches?.size ?: 0 }
|
||||
}.collectAsStateWithLifecycle(0)
|
||||
val props by remember(bundle) {
|
||||
bundle.propsOrNullFlow()
|
||||
}.collectAsStateWithLifecycle(null)
|
||||
|
||||
if (viewCurrentBundlePatches) {
|
||||
BundlePatchesDialog(
|
||||
onDismissRequest = {
|
||||
viewCurrentBundlePatches = false
|
||||
},
|
||||
source = source,
|
||||
bundle = bundle,
|
||||
)
|
||||
}
|
||||
|
||||
@ -75,13 +76,15 @@ fun BundleInformationDialog(
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onDeleteRequest) {
|
||||
Icon(
|
||||
Icons.Outlined.DeleteOutline,
|
||||
stringResource(R.string.delete)
|
||||
)
|
||||
if (!bundle.isDefault) {
|
||||
IconButton(onClick = onDeleteRequest) {
|
||||
Icon(
|
||||
Icons.Outlined.DeleteOutline,
|
||||
stringResource(R.string.delete)
|
||||
)
|
||||
}
|
||||
}
|
||||
if(!isLocal) {
|
||||
if (!isLocal) {
|
||||
IconButton(onClick = onRefreshButton) {
|
||||
Icon(
|
||||
Icons.Outlined.Refresh,
|
||||
@ -93,51 +96,23 @@ fun BundleInformationDialog(
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = 24.dp,
|
||||
top = 16.dp,
|
||||
end = 24.dp,
|
||||
)
|
||||
) {
|
||||
BundleTextContent(
|
||||
name = source.name,
|
||||
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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
BaseBundleDialog(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
isDefault = bundle.isDefault,
|
||||
name = bundle.name,
|
||||
remoteUrl = bundle.asRemoteOrNull?.endpoint,
|
||||
patchCount = patchCount,
|
||||
version = props?.versionInfo?.patches,
|
||||
autoUpdate = props?.autoUpdate ?: false,
|
||||
onAutoUpdateChange = {
|
||||
composableScope.launch {
|
||||
bundle.asRemoteOrNull?.setAutoUpdate(it)
|
||||
}
|
||||
},
|
||||
onPatchesClick = {
|
||||
viewCurrentBundlePatches = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
@Composable
|
||||
fun BundleInfoListItem(
|
||||
fun BundleListItem(
|
||||
headlineText: String,
|
||||
supportingText: String = "",
|
||||
trailingContent: @Composable (() -> Unit)? = null,
|
@ -28,17 +28,17 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BundlePatchesDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
source: Source,
|
||||
bundle: PatchBundleSource,
|
||||
) {
|
||||
var informationCardVisible by remember { mutableStateOf(true) }
|
||||
val bundle by source.bundle.collectAsStateWithLifecycle()
|
||||
val state by bundle.state.collectAsStateWithLifecycle()
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
@ -84,27 +84,29 @@ fun BundlePatchesDialog(
|
||||
}
|
||||
}
|
||||
|
||||
items(bundle.patches.size) { bundleIndex ->
|
||||
val patch = bundle.patches[bundleIndex]
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = patch.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
patch.description?.let {
|
||||
state.patchBundleOrNull()?.let { bundle ->
|
||||
items(bundle.patches.size) { bundleIndex ->
|
||||
val patch = bundle.patches[bundleIndex]
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
text = patch.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
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.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.domain.sources.Source
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SourceSelector(sources: List<Source>, onFinish: (Source?) -> Unit) {
|
||||
LaunchedEffect(sources) {
|
||||
if (sources.size == 1) {
|
||||
onFinish(sources[0])
|
||||
fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSource?) -> Unit) {
|
||||
LaunchedEffect(bundles) {
|
||||
if (bundles.size == 1) {
|
||||
onFinish(bundles[0])
|
||||
}
|
||||
}
|
||||
|
||||
if (sources.size < 2) {
|
||||
if (bundles.size < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ fun SourceSelector(sources: List<Source>, onFinish: (Source?) -> Unit) {
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
sources.forEach {
|
||||
bundles.forEach {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
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
|
||||
|
||||
import android.net.Uri
|
||||
import android.webkit.URLUtil
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
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.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Topic
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -34,20 +31,17 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.util.APK_MIMETYPE
|
||||
import app.revanced.manager.util.JAR_MIMETYPE
|
||||
import app.revanced.manager.util.parseUrlOrNull
|
||||
import io.ktor.http.Url
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ImportBundleDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onRemoteSubmit: (String, Url) -> Unit,
|
||||
onLocalSubmit: (String, Uri, Uri?) -> Unit,
|
||||
patchCount: Int = 0,
|
||||
onRemoteSubmit: (String, String, Boolean) -> Unit,
|
||||
onLocalSubmit: (String, Uri, Uri?) -> Unit
|
||||
) {
|
||||
var name 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 patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||
var integrations by rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||
@ -58,8 +52,10 @@ fun ImportBundleDialog(
|
||||
val inputsAreValid by remember {
|
||||
derivedStateOf {
|
||||
val nameSize = name.length
|
||||
nameSize in 4..19 && if (isLocal) patchBundle != null else {
|
||||
remoteUrl.isNotEmpty() && remoteUrl.parseUrlOrNull() != null
|
||||
when {
|
||||
nameSize !in 1..19 -> false
|
||||
isLocal -> patchBundle != null
|
||||
else -> remoteUrl.isNotEmpty() && URLUtil.isValidUrl(remoteUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -68,19 +64,11 @@ fun ImportBundleDialog(
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
uri?.let { patchBundle = it }
|
||||
}
|
||||
|
||||
val integrationsActivityLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
uri?.let { integrations = it }
|
||||
}
|
||||
|
||||
val onPatchLauncherClick = {
|
||||
patchActivityLauncher.launch(JAR_MIMETYPE)
|
||||
}
|
||||
|
||||
val onIntegrationLauncherClick = {
|
||||
integrationsActivityLauncher.launch(APK_MIMETYPE)
|
||||
}
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = DialogProperties(
|
||||
@ -100,115 +88,89 @@ fun ImportBundleDialog(
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
Text(
|
||||
text = stringResource(R.string.import_),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.clickable {
|
||||
if (inputsAreValid) {
|
||||
if (isLocal) {
|
||||
onLocalSubmit(name, patchBundle!!, integrations)
|
||||
} else {
|
||||
onRemoteSubmit(name, remoteUrl.parseUrlOrNull()!!)
|
||||
}
|
||||
}
|
||||
TextButton(
|
||||
enabled = inputsAreValid,
|
||||
onClick = {
|
||||
if (isLocal) {
|
||||
onLocalSubmit(name, patchBundle!!, integrations)
|
||||
} else {
|
||||
onRemoteSubmit(
|
||||
name,
|
||||
remoteUrl,
|
||||
autoUpdate
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(end = 16.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.import_))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
BaseBundleDialog(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
isDefault = false,
|
||||
name = name,
|
||||
onNameChange = { name = it },
|
||||
remoteUrl = remoteUrl.takeUnless { isLocal },
|
||||
onRemoteUrlChange = { remoteUrl = it },
|
||||
patchCount = 0,
|
||||
version = null,
|
||||
autoUpdate = autoUpdate,
|
||||
onAutoUpdateChange = { autoUpdate = it },
|
||||
onPatchesClick = {},
|
||||
onBundleTypeClick = { isLocal = !isLocal },
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = 24.dp,
|
||||
top = 16.dp,
|
||||
end = 24.dp,
|
||||
)
|
||||
) {
|
||||
BundleTextContent(
|
||||
name = name,
|
||||
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))
|
||||
}
|
||||
if (isLocal) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
value = patchBundleText,
|
||||
onValueChange = {},
|
||||
label = {
|
||||
Text("Patches Source File")
|
||||
},
|
||||
tonalButtonOnClick = { isLocal = !isLocal },
|
||||
isLocal = isLocal,
|
||||
trailingIcon = {
|
||||
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.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.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 org.koin.androidx.compose.getViewModel
|
||||
|
||||
enum class DashboardPage(
|
||||
val titleResId: Int,
|
||||
val icon: ImageVector
|
||||
) {
|
||||
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)
|
||||
@Composable
|
||||
fun DashboardScreen(
|
||||
vm: DashboardViewModel = getViewModel(),
|
||||
onAppSelectorClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
) {
|
||||
var showImportBundleDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val pages: Array<DashboardPage> = DashboardPage.values()
|
||||
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
||||
val androidContext = LocalContext.current
|
||||
|
||||
val pagerState = rememberPagerState()
|
||||
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(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
@ -66,10 +98,28 @@ fun DashboardScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = {
|
||||
if (pagerState.currentPage == DashboardPage.DASHBOARD.ordinal)
|
||||
onAppSelectorClick()
|
||||
}
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
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))
|
||||
}
|
||||
@ -103,8 +153,8 @@ fun DashboardScreen(
|
||||
InstalledAppsScreen()
|
||||
}
|
||||
|
||||
DashboardPage.SOURCES -> {
|
||||
SourcesScreen()
|
||||
DashboardPage.BUNDLES -> {
|
||||
BundlesScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ fun PatchesSelectorScreen(
|
||||
val pagerState = rememberPagerState()
|
||||
val composableScope = rememberCoroutineScope()
|
||||
|
||||
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyArray())
|
||||
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
if (vm.compatibleVersions.isNotEmpty())
|
||||
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
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Http
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
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.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AdvancedSettingsScreen(onBackClick: () -> Unit) {
|
||||
fun AdvancedSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: AdvancedSettingsViewModel = getViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val memoryLimit = remember {
|
||||
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
context.getString(R.string.device_memory_limit_format, activityManager.memoryClass, activityManager.largeMemoryClass)
|
||||
val activityManager = context.getSystemService<ActivityManager>()!!
|
||||
context.getString(
|
||||
R.string.device_memory_limit_format,
|
||||
activityManager.memoryClass,
|
||||
activityManager.largeMemoryClass
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
@ -43,6 +68,37 @@ fun AdvancedSettingsScreen(onBackClick: () -> Unit) {
|
||||
.padding(paddingValues)
|
||||
.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))
|
||||
ListItem(
|
||||
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.GroupHeader
|
||||
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 kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
@ -63,12 +63,12 @@ fun ImportExportSettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (vm.selectedSource == null) {
|
||||
SourceSelector(sources) {
|
||||
if (vm.selectedBundle == null) {
|
||||
BundleSelector(sources) {
|
||||
if (it == null) {
|
||||
vm.clearSelectionAction()
|
||||
} else {
|
||||
vm.selectSource(it)
|
||||
vm.selectBundle(it)
|
||||
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.repository.PatchSelectionRepository
|
||||
import app.revanced.manager.domain.repository.SerializedSelection
|
||||
import app.revanced.manager.domain.repository.SourceRepository
|
||||
import app.revanced.manager.domain.sources.Source
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.util.JSON_MIMETYPE
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
@ -37,11 +37,11 @@ class ImportExportViewModel(
|
||||
private val app: Application,
|
||||
private val keystoreManager: KeystoreManager,
|
||||
private val selectionRepository: PatchSelectionRepository,
|
||||
sourceRepository: SourceRepository
|
||||
patchBundleRepository: PatchBundleRepository
|
||||
) : ViewModel() {
|
||||
private val contentResolver = app.contentResolver
|
||||
val sources = sourceRepository.sources
|
||||
var selectedSource by mutableStateOf<Source?>(null)
|
||||
val sources = patchBundleRepository.sources
|
||||
var selectedBundle by mutableStateOf<PatchBundleSource?>(null)
|
||||
private set
|
||||
var selectionAction by mutableStateOf<SelectionAction?>(null)
|
||||
private set
|
||||
@ -107,20 +107,20 @@ class ImportExportViewModel(
|
||||
}
|
||||
|
||||
fun executeSelectionAction(target: Uri) = viewModelScope.launch {
|
||||
val source = selectedSource!!
|
||||
val source = selectedBundle!!
|
||||
val action = selectionAction!!
|
||||
clearSelectionAction()
|
||||
|
||||
action.execute(source, target)
|
||||
action.execute(source.uid, target)
|
||||
}
|
||||
|
||||
fun selectSource(source: Source) {
|
||||
selectedSource = source
|
||||
fun selectBundle(bundle: PatchBundleSource) {
|
||||
selectedBundle = bundle
|
||||
}
|
||||
|
||||
fun clearSelectionAction() {
|
||||
selectionAction = null
|
||||
selectedSource = null
|
||||
selectedBundle = null
|
||||
}
|
||||
|
||||
fun importSelection() = clearSelectionAction().also {
|
||||
@ -132,7 +132,7 @@ class ImportExportViewModel(
|
||||
}
|
||||
|
||||
sealed interface SelectionAction {
|
||||
suspend fun execute(source: Source, location: Uri)
|
||||
suspend fun execute(bundleUid: Int, location: Uri)
|
||||
val activityContract: ActivityResultContract<String, Uri?>
|
||||
val activityArg: String
|
||||
}
|
||||
@ -140,7 +140,7 @@ class ImportExportViewModel(
|
||||
private inner class Import : SelectionAction {
|
||||
override val activityContract = ActivityResultContracts.GetContent()
|
||||
override val activityArg = JSON_MIMETYPE
|
||||
override suspend fun execute(source: Source, location: Uri) = uiSafe(
|
||||
override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe(
|
||||
app,
|
||||
R.string.restore_patches_selection_fail,
|
||||
"Failed to restore patches selection"
|
||||
@ -151,19 +151,19 @@ class ImportExportViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
selectionRepository.import(source, selection)
|
||||
selectionRepository.import(bundleUid, selection)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class Export : SelectionAction {
|
||||
override val activityContract = ActivityResultContracts.CreateDocument(JSON_MIMETYPE)
|
||||
override val activityArg = "selection.json"
|
||||
override suspend fun execute(source: Source, location: Uri) = uiSafe(
|
||||
override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe(
|
||||
app,
|
||||
R.string.backup_patches_selection_fail,
|
||||
"Failed to backup patches selection"
|
||||
) {
|
||||
val selection = selectionRepository.export(source)
|
||||
val selection = selectionRepository.export(bundleUid)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
contentResolver.openOutputStream(location, "wt")!!.use {
|
||||
|
@ -2,16 +2,31 @@ package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
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
|
||||
|
||||
class MainViewModel(
|
||||
sourceRepository: SourceRepository
|
||||
private val patchBundleRepository: PatchBundleRepository,
|
||||
val prefs: PreferencesManager
|
||||
) : ViewModel() {
|
||||
init {
|
||||
with(viewModelScope) {
|
||||
launch {
|
||||
sourceRepository.loadSources()
|
||||
|
||||
fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch {
|
||||
prefs.showAutoUpdatesDialog.update(false)
|
||||
|
||||
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 app.revanced.manager.domain.manager.PreferencesManager
|
||||
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.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.Options
|
||||
@ -43,21 +43,23 @@ class PatchesSelectorViewModel(
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
|
||||
val allowExperimental = get<PreferencesManager>().allowExperimental
|
||||
val bundlesFlow = get<SourceRepository>().sources.flatMapLatestAndCombine(
|
||||
combiner = { it }
|
||||
val bundlesFlow = get<PatchBundleRepository>().sources.flatMapLatestAndCombine(
|
||||
combiner = { it.filterNotNull() }
|
||||
) { source ->
|
||||
// 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 unsupported = mutableListOf<PatchInfo>()
|
||||
val universal = mutableListOf<PatchInfo>()
|
||||
|
||||
bundle.patches.filter { it.compatibleWith(selectedApp.packageName) }.forEach {
|
||||
val targetList =
|
||||
if (it.compatiblePackages == null) universal else if (it.supportsVersion(selectedApp.version))
|
||||
supported
|
||||
else
|
||||
unsupported
|
||||
val targetList = when {
|
||||
it.compatiblePackages == null -> universal
|
||||
it.supportsVersion(selectedApp.version) -> supported
|
||||
else -> unsupported
|
||||
}
|
||||
|
||||
targetList.add(it)
|
||||
}
|
||||
@ -66,30 +68,36 @@ class PatchesSelectorViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private val selectedPatches: SnapshotStatePatchesSelection by savedStateHandle.saveable(saver = patchesSelectionSaver, init = {
|
||||
val map: SnapshotStatePatchesSelection = mutableStateMapOf()
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val bundles = bundlesFlow.first()
|
||||
val filteredSelection =
|
||||
selectionRepository.getSelection(selectedApp.packageName).mapValues { (uid, patches) ->
|
||||
// Filter out patches that don't exist.
|
||||
val filteredPatches = bundles.singleOrNull { it.uid == uid }
|
||||
?.let { bundle ->
|
||||
val allPatches = bundle.all.map { it.name }
|
||||
patches.filter { allPatches.contains(it) }
|
||||
private val selectedPatches: SnapshotStatePatchesSelection by savedStateHandle.saveable(
|
||||
saver = patchesSelectionSaver,
|
||||
init = {
|
||||
val map: SnapshotStatePatchesSelection = mutableStateMapOf()
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val bundles = bundlesFlow.first()
|
||||
val filteredSelection =
|
||||
selectionRepository.getSelection(selectedApp.packageName)
|
||||
.mapValues { (uid, patches) ->
|
||||
// Filter out patches that don't exist.
|
||||
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
|
||||
})
|
||||
private val patchOptions: SnapshotStateOptions by savedStateHandle.saveable(saver = optionsSaver, init = ::mutableStateMapOf)
|
||||
return@saveable map
|
||||
})
|
||||
private val patchOptions: SnapshotStateOptions by savedStateHandle.saveable(
|
||||
saver = optionsSaver,
|
||||
init = ::mutableStateMapOf
|
||||
)
|
||||
|
||||
/**
|
||||
* 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.viewModelScope
|
||||
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.AppDownloader
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
@ -28,7 +28,7 @@ class VersionSelectorViewModel(
|
||||
val packageName: String
|
||||
) : ViewModel(), KoinComponent {
|
||||
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 appDownloader: AppDownloader = APKMirror()
|
||||
|
||||
@ -41,7 +41,7 @@ class VersionSelectorViewModel(
|
||||
|
||||
val downloadableVersions = mutableStateSetOf<SelectedApp.Download>()
|
||||
|
||||
val supportedVersions = sourceRepository.bundles.map { bundles ->
|
||||
val supportedVersions = patchBundleRepository.bundles.map { bundles ->
|
||||
var patchesWithoutVersions = 0
|
||||
|
||||
bundles.flatMap { (_, bundle) ->
|
||||
|
@ -8,7 +8,6 @@ const val ghPatcher = "$team/revanced-patcher"
|
||||
const val ghManager = "$team/revanced-manager"
|
||||
const val ghIntegrations = "$team/revanced-integrations"
|
||||
const val tag = "ReVanced Manager"
|
||||
const val apiURL = "https://releases.revanced.app"
|
||||
|
||||
const val JAR_MIMETYPE = "application/java-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.Parcelable
|
||||
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.UninstallService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -40,11 +40,11 @@ data class AppInfo(
|
||||
@Suppress("DEPRECATION")
|
||||
class PM(
|
||||
private val app: Application,
|
||||
sourceRepository: SourceRepository
|
||||
patchBundleRepository: PatchBundleRepository
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
val appList = sourceRepository.bundles.map { bundles ->
|
||||
val appList = patchBundleRepository.bundles.map { bundles ->
|
||||
val compatibleApps = scope.async {
|
||||
val compatiblePackages = bundles.values
|
||||
.flatMap { it.patches }
|
||||
|
@ -11,7 +11,6 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import io.ktor.http.Url
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
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()
|
||||
}
|
||||
|
||||
fun String.parseUrlOrNull() = try {
|
||||
Url(this)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -12,11 +12,20 @@
|
||||
<string name="select_patches">Select patches</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_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="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_description">General settings</string>
|
||||
@ -90,22 +99,30 @@
|
||||
<string name="dark">Dark</string>
|
||||
<string name="appearance">Appearance</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_android_version">Android version</string>
|
||||
<string name="device_model">Model</string>
|
||||
<string name="device_architectures">CPU Architectures</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="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="signing">Signing</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_sources">Sources</string>
|
||||
<string name="tab_bundles">Patch bundles</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="refresh">Refresh</string>
|
||||
<string name="remote">Remote</string>
|
||||
<string name="local">Local</string>
|
||||
<string name="reload_sources">Reload all sources</string>
|
||||
<string name="continue_anyways">Continue anyways</string>
|
||||
<string name="download_another_version">Download another version</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="bundle_type">Bundle type</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="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>
|
||||
@ -199,6 +214,7 @@
|
||||
<string name="downloading_manager_update">Downloading update…</string>
|
||||
<string name="download_manager_failed">Failed to download update: %s</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="save">Save</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>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user